From d4c1e0a264395a82a13bd241eee88817a184389f Mon Sep 17 00:00:00 2001 From: George Date: Wed, 14 Oct 2026 22:49:11 -0500 Subject: [PATCH] =?UTF-8?q?se=20agrega=20README=20y=20se=20completa=20y=20?= =?UTF-8?q?corrige=20la=20documentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 79 +++++++++++++++++++ docs/construir_draft.md | 40 ++++++++++ docs/construir_draft.mkd | 35 -------- docs/hana_manager.md | 56 +++++++++++++ docs/inventario.md | 30 +++---- docs/outlook_utilities.md | 40 ++++++++++ ...validar_precios.mkd => validar_precios.md} | 15 +++- draft_sap.py | 12 +-- outlook_utilities.py | 2 +- 9 files changed, 249 insertions(+), 60 deletions(-) create mode 100644 README.md create mode 100644 docs/construir_draft.md delete mode 100644 docs/construir_draft.mkd create mode 100644 docs/hana_manager.md create mode 100644 docs/outlook_utilities.md rename docs/{validar_precios.mkd => validar_precios.md} (60%) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff9b5d7 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# descarga_factura_transporte + +Demonio que descarga las facturas de transporte (PDF/XML SUNAT) recibidas en un buzón de Office 365, valida sus precios contra SAP Business One y crea el borrador (Draft) de la factura de compra. Al terminar, notifica el resultado por correo al remitente. + +## Qué hace con cada correo + +``` +Bandeja de entrada (Microsoft Graph) + → descarga adjuntos, descomprime ZIP, se queda con PDF/XML + → extrae datos de PDF y XML y asigna las guías de remisión por línea + → valida precios de cada guía contra SAP (Service Layer, con fallback a HANA) + → crea el Draft en SAP B1 si todos los precios coinciden + → envía correo con el resultado (Draft creado / error / precios no coinciden) +``` + +Cada correo se registra en una base SQLite (`emails.db`) y no se vuelve a procesar. Detalle del flujo en [`docs/pipeline.md`](docs/pipeline.md). + +## Requisitos + +- Python 3.11 o superior. +- Acceso a: Microsoft Graph (app registrada en Azure AD), SAP B1 Service Layer y SAP HANA. + +## Instalación + +```bash +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env # y completar los valores +``` + +El `.env` nunca se sube a git. Todas las variables están documentadas en [`.env.example`](.env.example). Al arrancar, `config_validator` termina con un error claro si falta alguna obligatoria. + +## Uso + +```bash +python main.py # demonio; consulta los últimos 2 días (--dias 2) +python main.py --dias 5 # ventana de búsqueda de 5 días +python tools/consulta_rapida.py --set # reprocesar correos por ID (sets en tools/ids_prueba.json) +``` + +El demonio trabaja por bloques horarios (cada 1, 5, 30 o 60 minutos según la hora) y duerme fuera de ellos. Los bloques están en `HORARIOS` de `main.py` y se explican en [`docs/main.md`](docs/main.md). + +## Carpetas de salida (ignoradas por git) + +| Ruta | Contenido | +|------|-----------| +| `adjuntos/` | PDF/XML descargados (`DOWNLOAD_FOLDER`) | +| `resultado/` | JSON de datos extraídos y de drafts, para depuración (`OUTPUT_FOLDER`) | +| `logs/` | Log con rotación diaria y 30 días de historial. `LOG_LEVEL` controla el nivel | +| `emails.db` | Correos ya procesados | + +## Estructura del proyecto + +| Módulo | Rol | +|--------|-----| +| `main.py` | Demonio y horarios | +| `pipeline.py` | Orquesta el proceso de cada correo | +| `adjuntos.py`, `documentos.py`, `persistencia.py` | Descarga de adjuntos, extracción de PDF/XML, guardado de JSON | +| `draft_sap.py`, `notificaciones.py` | Validación de precios y construcción del Draft; cuerpo de los correos | +| `outlook_utilities.py`, `sapb1_manager.py`, `hana_manager.py` | Clientes de Microsoft Graph, SAP Service Layer y SAP HANA | +| `sunat_schema.py`, `pdf_utilities.py` | Lectura de XML SUNAT y de PDF | +| `database_utilities.py`, `email_utilities.py` | SQLite local; ZIP y filtrado de adjuntos | +| `logger_manager.py`, `config_validator.py`, `pipeline_utils.py` | Logging, validación del `.env`, utilidades | +| `tools/` | Scripts manuales (`consulta_rapida.py`, `aberio.http`, datos de prueba) | +| `tests/` | Scripts de prueba manual y correos de ejemplo (`tests/correos_prueba/`) | + +## Documentación + +| Documento | Tema | +|-----------|------| +| [`docs/inventario.md`](docs/inventario.md) | Archivos, dependencias, flujo de datos y bugs corregidos | +| [`docs/main.md`](docs/main.md) | Demonio y horarios | +| [`docs/pipeline.md`](docs/pipeline.md) | Flujo por correo y funciones por módulo | +| [`docs/procesar_pdfs.md`](docs/procesar_pdfs.md) / [`docs/procesar_xmls.md`](docs/procesar_xmls.md) | Extracción de datos de PDF y XML | +| [`docs/validar_precios.md`](docs/validar_precios.md) | Validación de precios contra SAP | +| [`docs/construir_draft.md`](docs/construir_draft.md) | Construcción del Draft SAP B1 | +| [`docs/outlook_utilities.md`](docs/outlook_utilities.md) | Cliente de Microsoft Graph | +| [`docs/hana_manager.md`](docs/hana_manager.md) | Consultas directas a SAP HANA | diff --git a/docs/construir_draft.md b/docs/construir_draft.md new file mode 100644 index 0000000..e5db7eb --- /dev/null +++ b/docs/construir_draft.md @@ -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
(de validate_prices)"] --> B["ItemCode según fecha_emision:
C103002-A (mes anterior)
C103002 (mes actual)"] + B --> B2["CardCode = PL + ruc_emisor
sapb1_manager.obtener_condicion_pago(CardCode)
(BusinessPartners.PayTermsGrpCode → PaymentTermsTypes)"] + B2 --> B3["DocDueDate = fecha_contabilizacion (hoy) +
NumberOfAdditionalDays
(si es 0, NumberOfAdditionalMonths * 30)"] + B3 --> C["Calcular cuotas
DocumentInstallments"] + + C --> D{"¿Tiene detracción?"} + D -->|Sí| E["monto_detraccion = ceil(monto)
Cuota 1: monto_detraccion
U_VS_TIPOCUOTA = D
U_VS_TIPOSERVICIO = codigo + '02'"] + E --> F["Cuota 2: neto = total - monto_detraccion
U_VS_TIPOCUOTA = N"] + D -->|No| G["Cuota única por el total
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']
(si no hay, se salta la línea)"] + I --> K["Por cada item en guias_lt[]"] + + K --> L["PriceAfterVAT = draft_price
truncado a 6 decimales"] + L --> M["Quantity = draft_quantity"] + M --> N["Crear DocumentLine:
ItemCode, Quantity, PriceAfterVAT,
WarehouseCode, FreeText (descripción[:100]),
campos fijos U_* / Costing / TaxCode I18,
U_IDC_GRFLETE = tipo+serie-numero de la guía"] + N --> K + + K --> P["U_IDC_VALREF = suma de valor_referencial
de las líneas del XML"] + P --> Q["U_IDC_NUMGR = primera guía de la
última línea que tuvo guías"] + Q --> R["Cabecera: DocType, DocDate = hoy,
DocDueDate, TaxDate = fecha_emision,
CardCode, NumAtCard, Comments,
U_VS_ANEXO_PDF / U_VS_ANEXO_XML
(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. diff --git a/docs/construir_draft.mkd b/docs/construir_draft.mkd deleted file mode 100644 index 527b8b2..0000000 --- a/docs/construir_draft.mkd +++ /dev/null @@ -1,35 +0,0 @@ -# Construir Draft SAP B1 - -```mermaid -flowchart TD - A["factura + resultados_val"] --> B["Determinar ItemCode
C103002-A (mes anterior)
C103002 (mes actual)"] - B --> B2["CardCode = PL + ruc_emisor
sapb1_manager.obtener_condicion_pago(CardCode)
(BusinessPartners.PayTermsGrpCode → PaymentTermsTypes)"] - B2 --> B3["fecha_vencimiento = fecha_contabilizacion +
NumberOfAdditionalDays
(si es 0, usar NumberOfAdditionalMonths * 30)"] - B3 --> C["Calcular cuotas
DocumentInstallments"] - - C --> D{"¿Tiene detracción?"} - D -->|Sí| E["Cuota 1: monto detracción
U_VS_TIPOCUOTA = D"] - E --> F["Cuota 2: neto = total - detracción
U_VS_TIPOCUOTA = N"] - D -->|No| G["Cuota única por el total
U_VS_TIPOCUOTA = N"] - - F --> H["Por cada línea de factura"] - G --> H - - H --> I["Obtener guias_lt[]
desde resultado_validacion"] - I --> J["zona_guia = guias_lt[0]['zona']"] - J --> K["Por cada item en guias_lt[]"] - - K --> L["PriceAfterVAT:
suma pav FL* si use_sap_prices
line_total si no"] - L --> M["Qty = item['quantity']"] - M --> N["Crear DocumentLine:
ItemCode, Quantity,
PriceAfterVAT, WarehouseCode,
U_IDC_GRFLETE"] - - N --> O["Acumular total_weight
+= item['weight1']"] - O --> K - - K --> P["U_IDC_VALREF =
round((total_weight / 1000) *
CUADRO_VALOR_REFERENCIAL[zona], 2)"] - - P --> Q["Armar payload completo"] - Q --> R["Cabecera:
DocType, CardCode, NumAtCard,
DocDate = fecha_contabilizacion,
DocDueDate = fecha_vencimiento,
U_IDC_NUMGR, Comments"] - R --> S["DocumentLines[] +
DocumentInstallments[]"] - S --> T["← Retornar dict para
POST /Drafts"] -``` diff --git a/docs/hana_manager.md b/docs/hana_manager.md new file mode 100644 index 0000000..ed5fb3e --- /dev/null +++ b/docs/hana_manager.md @@ -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 ""."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 (`?`). diff --git a/docs/inventario.md b/docs/inventario.md index 2df28bb..372d3bb 100644 --- a/docs/inventario.md +++ b/docs/inventario.md @@ -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). --- diff --git a/docs/outlook_utilities.md b/docs/outlook_utilities.md new file mode 100644 index 0000000..2d986c4 --- /dev/null +++ b/docs/outlook_utilities.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. diff --git a/docs/validar_precios.mkd b/docs/validar_precios.md similarity index 60% rename from docs/validar_precios.mkd rename to docs/validar_precios.md index 66f889f..1e438eb 100644 --- a/docs/validar_precios.mkd +++ b/docs/validar_precios.md @@ -30,12 +30,19 @@ 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?"} - T -->|Sí| U["ok = True
use_sap_prices si qty==1
y total_sap_qty≠1"] - T -->|No| V["ok = False"] + S --> T{"abs(diff) < 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"] - U --> W["Resultado por línea:
ok, draft_price, sap_price,
draft_cantidad, guias_lt[]"] + U --> W["Resultado por línea:
ok, draft_price, sap_price,
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)`. diff --git a/draft_sap.py b/draft_sap.py index 0ca6f8b..3e83639 100644 --- a/draft_sap.py +++ b/draft_sap.py @@ -72,7 +72,7 @@ def build_sap_draft(factura: dict, resultados_val: list[dict]) -> dict: ``guias_remision[]``, ``tiene_detraccion``, etc.). resultados_val: Lista de resultados de ``validate_prices``, uno por línea de factura. Cada elemento contiene - ``guias_lt[]``, ``ok``, ``use_sap_prices``, etc. + ``guias_lt[]``, ``ok``, etc. Returns: Dict con la estructura completa del draft SAP B1 lista para @@ -226,9 +226,9 @@ def validate_prices(factura: dict) -> tuple[bool, list[dict]]: ``warehouse_code`` y ``zona``. 3. Determina si los precios coinciden (``ok``): ``abs(total_linea - sap_total) < 0.01``. - 4. Decide si se deben usar precios de SAP en el draft - (``use_sap_prices``): - ``ok and int(cantidad) == 1 and int(total_sap_qty) != 1``. + 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``). Args: factura: Diccionario con los datos de la factura (debe contener @@ -241,8 +241,8 @@ def validate_prices(factura: dict) -> tuple[bool, list[dict]]: ``ok=True``. - ``resultados``: Lista de resultados por línea, cada uno con claves: ``grflete``, ``ok``, ``draft_price``, - ``sap_price``, ``warehouse_code``, ``draft_cantidad``, - ``use_sap_prices``, ``guias_lt[]``. + ``sap_price``, ``label``, ``warehouse_code``, ``guias_lt[]`` + (y ``draft_cantidad`` en las líneas sin validar). Example: Factura con guía ``T001-127773`` y SAP devuelve diff --git a/outlook_utilities.py b/outlook_utilities.py index ba21007..8668588 100644 --- a/outlook_utilities.py +++ b/outlook_utilities.py @@ -29,7 +29,7 @@ VERIFY_SSL = os.getenv("VERIFY_SSL", "False").lower() == "true" def get_access_token() -> str: """ Obtiene un token de acceso de Microsoft Graph usando las credenciales - configuradas en config.json (Client ID, Client Secret y Tenant ID). + del .env (CLIENT_ID, CLIENT_SECRET y TENANT_ID). """ authority = f"https://login.microsoftonline.com/{TENANT_ID}"