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:
GeorgeandClaude Sonnet 5.5 committed 2026-10-20 22:03:38 -05:00
1 parent 74be942a3e
commit b1b6f70983
9 files changed
+249 -60

No files matched your search

+79
View File
@@ -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 <nombre> # 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 |
+40
View File
@@ -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.
-35
View File
@@ -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"]
```
+56
View File
@@ -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
View File
@@ -4,24 +4,26 @@
| Archivo | Líneas | Rol | | Archivo | Líneas | Rol |
|---------|--------|-----| |---------|--------|-----|
| `main.py` | 134 | Demonio orquestador (loop de horarios) | | `main.py` | 137 | Demonio orquestador (loop de horarios) |
| `pipeline.py` | 230 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) | | `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()` | | `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 | | `adjuntos.py` | 82 | Descarga de adjuntos (Graph) y filtrado PDF/XML |
| `documentos.py` | 371 | Extracción PDF/XML, asignación de guías, copia a red | | `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 | | `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 | | `notificaciones.py` | 117 | Cuerpos HTML de los correos de notificación |
| `sunat_schema.py` | 720 | Parseo de XML SUNAT (Invoice, DespatchAdvice) | | `sunat_schema.py` | 722 | Parseo de XML SUNAT (Invoice, DespatchAdvice) |
| `pdf_utilities.py` | 464 | Parseo de PDFs (factura, guías) | | `pdf_utilities.py` | 471 | Parseo de PDFs (factura, guías) |
| `sapb1_manager.py` | 321 | Comunicación con SAP B1 Service Layer | | `sapb1_manager.py` | 401 | Comunicación con SAP B1 Service Layer |
| `outlook_utilities.py` | 234 | Comunicación con Microsoft Graph | | `outlook_utilities.py` | 249 | Comunicación con Microsoft Graph |
| `hana_manager.py` | 123 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) | | `hana_manager.py` | 141 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) |
| `database_utilities.py` | 116 | SQLite local (cache de correos) | | `database_utilities.py` | 117 | SQLite local (cache de correos) |
| `tools/consulta_rapida.py` | 121 | Script auxiliar para reprocesar correos por ID (sets en `tools/ids_prueba.json`) | | `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) | | `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) | | `email_utilities.py` | 55 | Utilitarios de adjuntos (ZIP, filtrado) |
| `logger_manager.py` | 149 | Logging a archivo rotado (30 días) + consola + captura de `print()` | | `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).
--- ---
+40
View File
@@ -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"] 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*)"] R --> S["Comparar total_linea (XML)<br>vs sap_total (FL*)"]
S --> T{"abs(diff) < 0.01?"} S --> T{"abs(diff) < 0.01<br>y ninguna guía falló?"}
T -->|Sí| U["ok = True<br>use_sap_prices si qty==1<br>y total_sap_qty≠1"] T -->|Sí| U["ok = True<br>draft_quantity = int(Quantity máx FL*)<br>draft_price = suma PriceAfterVAT FL*"]
T -->|No| V["ok = False"] 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 V --> W
W --> C W --> C
C --> X["todas_coinciden = all(ok)"] 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)`.
+6 -6
View File
@@ -72,7 +72,7 @@ def build_sap_draft(factura: dict, resultados_val: list[dict]) -> dict:
``guias_remision[]``, ``tiene_detraccion``, etc.). ``guias_remision[]``, ``tiene_detraccion``, etc.).
resultados_val: Lista de resultados de ``validate_prices``, resultados_val: Lista de resultados de ``validate_prices``,
uno por línea de factura. Cada elemento contiene uno por línea de factura. Cada elemento contiene
``guias_lt[]``, ``ok``, ``use_sap_prices``, etc. ``guias_lt[]``, ``ok``, etc.
Returns: Returns:
Dict con la estructura completa del draft SAP B1 lista para 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``. ``warehouse_code`` y ``zona``.
3. Determina si los precios coinciden (``ok``): 3. Determina si los precios coinciden (``ok``):
``abs(total_linea - sap_total) < 0.01``. ``abs(total_linea - sap_total) < 0.01``.
4. Decide si se deben usar precios de SAP en el draft 4. Fija ``draft_quantity`` y ``draft_price`` de cada guía:
(``use_sap_prices``): si ``ok``, usa SAP (``int(quantity)`` y ``price_after_vat_sum``);
``ok and int(cantidad) == 1 and int(total_sap_qty) != 1``. si no, usa el XML (``int(cantidad)`` y ``total_linea / cantidad``).
Args: Args:
factura: Diccionario con los datos de la factura (debe contener 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``. ``ok=True``.
- ``resultados``: Lista de resultados por línea, cada uno - ``resultados``: Lista de resultados por línea, cada uno
con claves: ``grflete``, ``ok``, ``draft_price``, con claves: ``grflete``, ``ok``, ``draft_price``,
``sap_price``, ``warehouse_code``, ``draft_cantidad``, ``sap_price``, ``label``, ``warehouse_code``, ``guias_lt[]``
``use_sap_prices``, ``guias_lt[]``. (y ``draft_cantidad`` en las líneas sin validar).
Example: Example:
Factura con guía ``T001-127773`` y SAP devuelve Factura con guía ``T001-127773`` y SAP devuelve
+1 -1
View File
@@ -29,7 +29,7 @@ VERIFY_SSL = os.getenv("VERIFY_SSL", "False").lower() == "true"
def get_access_token() -> str: def get_access_token() -> str:
""" """
Obtiene un token de acceso de Microsoft Graph usando las credenciales 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}" authority = f"https://login.microsoftonline.com/{TENANT_ID}"