63 lines
4.6 KiB
Markdown
63 lines
4.6 KiB
Markdown
# 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.
|