# sapb1_manager.py — SAP Business One Service Layer Cliente del Service Layer de SAP B1: crea los drafts de factura de compra, ubica las guías de remisión para validar precios, busca si una factura ya existe y obtiene datos maestros. Si el Service Layer no encuentra una guía, recurre a HANA (`hana_manager.py`). ## Configuración (`.env`) | Variable | Para qué | Por defecto | |----------|----------|-------------| | `SAP_B1_URL` | URL del Service Layer (por ejemplo `https://servidor:50000/b1s/v1`) | — | | `SAP_B1_COMPANY`, `SAP_B1_USER`, `SAP_B1_PASSWORD` | Base de datos y credenciales | — | | `SAP_B1_TIMEOUT` | Segundos de espera por petición | `15` | | `SAP_B1_VERIFY_SSL` | Verificar el certificado (`true`) | `false` | ## Sesión La sesión se abre sola en la primera petición (`login`) y se guarda en un diccionario del módulo. Ante un HTTP 401 se vuelve a iniciar sesión **una vez** y se repite la petición. No hay cierre automático: `logout()` existe pero el demonio no lo llama. Al ser un estado global del módulo, no está pensado para varios hilos. ## Funciones | Función | Qué hace | |---------|----------| | `forward_to_sapb1(method, endpoint_path, json_data, params, extra_headers)` | Punto único de salida hacia SAP. Registra cada petición y clasifica los errores (ver abajo). | | `create_draft(payload)` | `POST /Drafts`. Devuelve el draft creado (con `DocEntry`). | | `buscar_factura_existente(ruc_emisor, tipo, serie, numero)` | ¿La factura ya está en SAP? Ver más abajo. | | `get_delivery_notes(tipo, serie, numero)` | Documento completo de una guía, con las líneas `FL*` de las que se validan los precios. | | `obtener_condicion_pago(card_code)` | Condición de pago del proveedor (para el vencimiento). | | `get_zonas()` | Maestro `U_IDC_ZONAS` (activas), para resolver el nombre de la zona de destino. | | `get_purchase_invoice(doc_entry)` | `GET /PurchaseInvoices(DocEntry)`. | | `login()` / `logout()` | Inicio y cierre de sesión. | ## Errores: caída o rechazo Todo error del Service Layer se lanza como `ConnectionError`, pero se distinguen dos casos: | Excepción | Cuándo | Qué hace el pipeline | |-----------|--------|----------------------| | **`SapNoDisponible`** (subclase de `ConnectionError`) | Timeout, sin conexión, **HTTP 5xx**, o login imposible por esos motivos | Es una **caída**: aviso "En cola" al proveedor y reintento del correo | | `ConnectionError` | HTTP 4xx (SAP rechazó la petición), credenciales inválidas | Es un error nuestro: se avisa solo al equipo | Así, una caída nunca se confunde con "la guía no existe" ni se le comunica al proveedor como rechazo. Detalle del flujo en [respuesta_proveedor.md](respuesta_proveedor.md). ## ¿La factura ya existe en SAP? `buscar_factura_existente` busca por proveedor (`CardCode = 'PL' + RUC`), tipo, serie y número (`U_BPP_MDTD`, `U_BPP_MDSD`, `U_BPP_MDCD`, los mismos campos que llena el draft) en dos entidades, en este orden: 1. `PurchaseInvoices`, solo las **no anuladas** (`Cancelled eq 'tNO'`). 2. `Drafts` de factura de compra (`DocObjectCode eq 'oPurchaseInvoices'`): una factura cuyo draft ya se creó aunque nadie lo haya contabilizado. Devuelve `{"entidad", "DocEntry", "DocNum"}` del primer documento hallado, o `None`. Los valores se escapan para el filtro OData (las comillas simples se duplican). Si SAP no responde lanza `SapNoDisponible`: no se puede afirmar que la factura no exista. ## Cómo se ubica una guía (`get_delivery_notes`) 1. **Service Layer:** `GET /DeliveryNotes` con `Cancelled eq 'tNO'`, el tipo, la serie y `contains(U_BPP_MDCD, número)` (coincidencia parcial del número). 2. **HANA**, si Service Layer no devolvió nada o falló: consulta `ODLN` y, si no hay resultados, `OWTR`. 3. Por cada documento hallado se hace `GET` a su entidad: `DeliveryNotes` ya trae las líneas `FL*`; en `StockTransfers` (traslados, `OWTR`) se lee `U_IDC_NUMOV` y se obtiene la **orden de venta** (`Orders`) asociada, que es la que lleva precios. Si Service Layer **cayó** y HANA no aporta nada, se lanza `SapNoDisponible` en vez de devolver una lista vacía; igual si la caída ocurre al leer el documento. Una lista vacía significa solo "la guía no existe". ## Notas - `obtener_condicion_pago` devuelve `None` ante **cualquier** error (incluida una caída): en ese caso el vencimiento del draft queda igual a la fecha de contabilización. - `get_zonas` no captura errores: si Service Layer no responde, lanza la excepción (y `validate_prices` la deja pasar como `SapNoDisponible`). - `SAP_B1_VERIFY_SSL` es `false` por defecto; en producción conviene activarlo con el certificado de la CA.