primer commit
This commit is contained in:
1 parent
ab3c613165
commit
b2fb4e2d0a
18 files changed
+1291
-277
No files matched your search
@@ -39,14 +39,17 @@ Todas las respuestas de error siguen el mismo formato:
|
||||
|
||||
### `POST /api/Login`
|
||||
|
||||
Inicia sesión de empleado. Valida contra SAP EmployeesInfo usando el apellido como contraseña.
|
||||
Inicia sesión de empleado. Busca en SAP `EmployeesInfo` al empleado cuyo
|
||||
campo de usuario `U_WEB_USER` coincida con `Username`, y valida `Password`
|
||||
contra su campo `U_WEB_PASS`. También exige que el empleado esté activo
|
||||
(`Active == "tYES"`).
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Username": "123",
|
||||
"Password": "Garcia"
|
||||
"Username": "jgarcia",
|
||||
"Password": "********"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -56,7 +59,8 @@ Inicia sesión de empleado. Valida contra SAP EmployeesInfo usando el apellido c
|
||||
{
|
||||
"message": "Login exitoso",
|
||||
"token": "aB3x...token_urlsafe_32_chars...",
|
||||
"username": "Juan"
|
||||
"username": "Juan",
|
||||
"company": "NOMBRE_COMPANYDB"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -68,6 +72,10 @@ Inicia sesión de empleado. Valida contra SAP EmployeesInfo usando el apellido c
|
||||
}
|
||||
```
|
||||
|
||||
Otros posibles `detail` en 401: `"Usuario no registrado"` (no existe ningún
|
||||
empleado con ese `U_WEB_USER`) y `"Usuario inactivo"` (el empleado existe
|
||||
pero `Active != "tYES"`).
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/Logout`
|
||||
@@ -364,6 +372,36 @@ Actualiza un pedido. Usa header `B1S-ReplaceCollectionsOnPatch: true`.
|
||||
|
||||
---
|
||||
|
||||
### Guías de Remisión de Venta (Delivery Notes)
|
||||
|
||||
#### `GET /api/DeliveryNotes`
|
||||
|
||||
Lista paginada de guías de remisión de venta. Mismos parámetros que Invoices/Orders.
|
||||
|
||||
#### `POST /api/DeliveryNotes`
|
||||
|
||||
Crea una guía de remisión de venta.
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
{
|
||||
"DocEntry": 3001
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/DeliveryNotes/{key}`
|
||||
|
||||
Obtiene una guía de remisión por `DocEntry`.
|
||||
|
||||
#### `PATCH /api/DeliveryNotes/{key}`
|
||||
|
||||
Actualiza una guía de remisión. Usa header `B1S-ReplaceCollectionsOnPatch: true`.
|
||||
|
||||
**Response 204:** Sin contenido.
|
||||
|
||||
---
|
||||
|
||||
## Compras
|
||||
|
||||
### Órdenes de Compra
|
||||
@@ -372,6 +410,18 @@ Actualiza un pedido. Usa header `B1S-ReplaceCollectionsOnPatch: true`.
|
||||
|
||||
Lista paginada. Mismos parámetros que Invoices/Orders.
|
||||
|
||||
#### `POST /api/PurchaseOrders`
|
||||
|
||||
Crea una orden de compra.
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
{
|
||||
"DocEntry": 4001
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/PurchaseOrders/{key}`
|
||||
|
||||
Obtiene una orden de compra por `DocEntry`.
|
||||
@@ -390,6 +440,18 @@ Actualiza una orden de compra. Usa header `B1S-ReplaceCollectionsOnPatch: true`.
|
||||
|
||||
Lista paginada. Mismos parámetros que Invoices/Orders.
|
||||
|
||||
#### `POST /api/PurchaseInvoices`
|
||||
|
||||
Crea una factura de compra.
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
{
|
||||
"DocEntry": 4501
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/PurchaseInvoices/{key}`
|
||||
|
||||
Obtiene una factura de compra por `DocEntry`.
|
||||
@@ -402,6 +464,36 @@ Actualiza una factura de compra. Usa header `B1S-ReplaceCollectionsOnPatch: true
|
||||
|
||||
---
|
||||
|
||||
### Guías de Recepción de Compra (Purchase Delivery Notes)
|
||||
|
||||
#### `GET /api/PurchaseDeliveryNotes`
|
||||
|
||||
Lista paginada. Mismos parámetros que Invoices/Orders.
|
||||
|
||||
#### `POST /api/PurchaseDeliveryNotes`
|
||||
|
||||
Crea una guía de recepción de compra.
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
{
|
||||
"DocEntry": 5001
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/PurchaseDeliveryNotes/{key}`
|
||||
|
||||
Obtiene una guía de recepción de compra por `DocEntry`.
|
||||
|
||||
#### `PATCH /api/PurchaseDeliveryNotes/{key}`
|
||||
|
||||
Actualiza una guía de recepción de compra. Usa header `B1S-ReplaceCollectionsOnPatch: true`.
|
||||
|
||||
**Response 204:** Sin contenido.
|
||||
|
||||
---
|
||||
|
||||
## Catálogos
|
||||
|
||||
### `GET /api/Currencies`
|
||||
@@ -465,6 +557,31 @@ Lista paginada de códigos de IVA. Acepta `skip`, `top`, `fields`, `filter`.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/FactoringIndicators`
|
||||
|
||||
Lista paginada de indicadores de factoring. Acepta `skip`, `top`, `fields`, `filter`.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/Sunat/BusinessPartners/DocumentType`
|
||||
|
||||
Lista los valores del campo de usuario `EXX_TIPODOCU` (tabla `OCRD`, tipo de
|
||||
documento SUNAT del socio de negocio), consultando `UserFieldsMD` en SAP.
|
||||
Endpoint fijo, sin parámetros de paginación.
|
||||
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"value": [
|
||||
{ "FieldID": 1, "Description": "DNI" },
|
||||
{ "FieldID": 2, "Description": "RUC" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lotes
|
||||
|
||||
### `GET /api/BatchNumberDetails`
|
||||
@@ -517,6 +634,93 @@ Retorna las rutas de menú permitidas para el empleado autenticado.
|
||||
|
||||
---
|
||||
|
||||
## Reportes (API Gateway — Crystal Reports)
|
||||
|
||||
Estos endpoints no van contra el Service Layer sino contra el **SAP API
|
||||
Gateway** (`core/api_gateway.py`), que expone los reportes Crystal Reports
|
||||
autorizados para la compañía.
|
||||
|
||||
### `GET /api/apigateway/reports`
|
||||
|
||||
Lista los reportes Crystal Reports autorizados para el usuario/compañía.
|
||||
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "DocCode": "R001", "DocName": "Guía de Remisión" }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/apigateway/reports/{docCode}/parameters`
|
||||
|
||||
Obtiene los parámetros de entrada que requiere un reporte puntual (por
|
||||
`DocCode`) antes de exportarlo.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/apigateway/reports/{docCode}/export`
|
||||
|
||||
Exporta el reporte a PDF. El body es la lista de parámetros que el reporte
|
||||
necesita (formato definido por el propio API Gateway / Crystal Reports).
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "DocEntry", "value": "1001" }
|
||||
]
|
||||
```
|
||||
|
||||
**Response 200:** `application/pdf` (streaming, `Content-Disposition: inline`).
|
||||
|
||||
---
|
||||
|
||||
## Reportes (Jinja/HTML → PDF)
|
||||
|
||||
Generados desde plantillas HTML propias (`templates/reports/`) renderizadas
|
||||
con Jinja2 y convertidas a PDF con WeasyPrint, a partir de datos consultados
|
||||
directamente en HANA (`core/hana.py`), sin pasar por el Service Layer.
|
||||
|
||||
### `POST /api/reports/stickers`
|
||||
|
||||
Genera el sticker de una guía de remisión de venta (`DeliveryNotes`) para
|
||||
imprimir etiquetas de despacho.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"doc_entry": 3001,
|
||||
"cantidad": 4,
|
||||
"formato": "pdf",
|
||||
"papel": "Carta"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Requerido | Descripción |
|
||||
|-------|------|-----------|-------------|
|
||||
| `doc_entry` | int | Sí | `DocEntry` de la guía de remisión en HANA |
|
||||
| `cantidad` | int | Sí | Cantidad de stickers a generar |
|
||||
| `formato` | `"pdf"` \| `"html"` | No (default `"pdf"`) | Formato de salida |
|
||||
| `papel` | `"A4"` \| `"Carta"` | No (default `"Carta"`) | Tamaño de papel del PDF |
|
||||
|
||||
**Response 200:** `application/pdf` o `text/html` según `formato`, streaming.
|
||||
|
||||
**Response 404:**
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "No se encontró el documento 3001"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 500:** error al consultar HANA.
|
||||
|
||||
---
|
||||
|
||||
## Resumen de Códigos de Respuesta
|
||||
|
||||
| Método | Éxito | Cuerpo en éxito | Error SAP | Error de conexión |
|
||||
|
||||
@@ -12,24 +12,32 @@ graph TB
|
||||
subgraph "Core"
|
||||
SAP[core/sap.py\nSAP Session & Proxy]
|
||||
SES[core/session.py\nEmployee Sessions]
|
||||
HANA[core/hana.py\nConexión directa HANA]
|
||||
AG[core/api_gateway.py\nSesión y Proxy APIGateway]
|
||||
CRUD[core/crud_router.py\nFactory de routers CRUD OData]
|
||||
end
|
||||
|
||||
subgraph "Routers"
|
||||
A[auth.py\nLogin / Logout]
|
||||
BP[business_partners.py\nSocios de Negocio]
|
||||
IT[items.py\nArtículos]
|
||||
SL[sales.py\nFacturas / Pedidos]
|
||||
PR[purchases.py\nÓrdenes / Facturas Compra]
|
||||
SL[sales.py\nFacturas / Pedidos / Guías]
|
||||
PR[purchases.py\nÓrdenes / Facturas / Guías Compra]
|
||||
CT[catalogs.py\nCatálogos]
|
||||
BT[batch.py\nLotes]
|
||||
PM[permissions.py\nPermisos de Menú]
|
||||
APG[apigateway.py\nCrystal Reports]
|
||||
RP[reports.py\nReportes Jinja/HTML → PDF]
|
||||
end
|
||||
|
||||
MP[menu_permissions.json]
|
||||
TPL[templates/reports/*.html]
|
||||
end
|
||||
|
||||
subgraph "External"
|
||||
SAP_SL[SAP B1 Service Layer]
|
||||
SAP_AG[SAP B1 API Gateway\nrs/v1/*]
|
||||
SAP_HANA[(SAP HANA)]
|
||||
end
|
||||
|
||||
M --> C
|
||||
@@ -44,21 +52,42 @@ graph TB
|
||||
M --> CT
|
||||
M --> BT
|
||||
M --> PM
|
||||
M --> APG
|
||||
M --> RP
|
||||
|
||||
A --> SAP
|
||||
A --> SES
|
||||
BP --> SAP
|
||||
IT --> SAP
|
||||
SL --> SAP
|
||||
PR --> SAP
|
||||
CT --> SAP
|
||||
BT --> SAP
|
||||
BP --> CRUD
|
||||
IT --> CRUD
|
||||
SL --> CRUD
|
||||
PR --> CRUD
|
||||
CT --> CRUD
|
||||
BT --> CRUD
|
||||
CRUD --> SAP
|
||||
PM --> MP
|
||||
APG --> AG
|
||||
RP --> HANA
|
||||
RP --> TPL
|
||||
|
||||
SAP --> SAP_SL
|
||||
SES --> SAP
|
||||
AG --> SAP_AG
|
||||
HANA --> SAP_HANA
|
||||
```
|
||||
|
||||
`business_partners.py`, `items.py`, `sales.py`, `purchases.py`, `batch.py` y
|
||||
la mayor parte de `catalogs.py` ya no implementan cada endpoint a mano: usan
|
||||
`core/crud_router.py`, una factory (`make_crud_router`) que genera el
|
||||
`list/get/create/update` típico de un recurso OData (mismo bloque de
|
||||
`$skip`/`$top`/`$filter` y llamada a `forward_to_sapb1`) a partir de un
|
||||
nombre de recurso y unos flags (si soporta `$orderby`, si expone `POST`,
|
||||
headers extra para el `PATCH`, etc). Esto evita que un copy-paste entre
|
||||
recursos deje, por ejemplo, un `PATCH` apuntando al endpoint de otro
|
||||
recurso. Un router de documentos de mercadeo (ventas o compras) puede
|
||||
generar varios recursos a la vez iterando esa factory, por eso `sales.py` y
|
||||
`purchases.py` ya no son un único router sino un `APIRouter` que incluye uno
|
||||
por cada tipo de documento.
|
||||
|
||||
## Pipeline de Middleware
|
||||
|
||||
Las peticiones atraviesan los siguientes middleware en orden:
|
||||
@@ -70,7 +99,7 @@ flowchart LR
|
||||
CID --> SES_MW[session_middleware\nValida X-Session-Token]
|
||||
SES_MW --> ROUTER{Router}
|
||||
ROUTER -->|/api/Login| A[auth.py]
|
||||
ROUTER -->|Otros /api/*| SAP_CORE[core/sap.py]
|
||||
ROUTER -->|Otros /api/*| DEST[routers → core/sap.py, core/api_gateway.py o core/hana.py]
|
||||
```
|
||||
|
||||
### Detalle de Middleware
|
||||
@@ -79,71 +108,86 @@ flowchart LR
|
||||
| -------------------------- | ----------------------------------------------------------- |
|
||||
| `CORSMiddleware` | Permite peticiones desde cualquier origen |
|
||||
| `correlation_id_middleware` | Genera o extrae `X-Request-ID` para trazabilidad en logs |
|
||||
| `session_middleware` | Valida token de sesión, adjunta `emp_id` a `request.state` |
|
||||
| `session_middleware` | Valida token de sesión, adjunta el usuario (`webUser`) a `request.state` y al contexto de logging |
|
||||
|
||||
## Arquitectura de Sesiones
|
||||
|
||||
Existen **dos capas de sesión independientes**:
|
||||
Existen **tres capas de sesión/credenciales independientes**:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Employee["Employee Session"]
|
||||
T["Token aleatorio → emp_id"]
|
||||
T["Token aleatorio → webUser"]
|
||||
E["employee_sessions dict"]
|
||||
TK["active_tokens dict"]
|
||||
T --> TK
|
||||
TK --> E
|
||||
end
|
||||
|
||||
subgraph SAP["SAP B1 Session"]
|
||||
subgraph SAP["SAP B1 Service Layer Session"]
|
||||
S["sap_session_store"]
|
||||
M["Manager Credentials (.env)"]
|
||||
S -.->|Renueva en 401| M
|
||||
MC["Manager Credentials (.env: SAPB1_SL_*)"]
|
||||
S -.->|Renueva en 401| MC
|
||||
end
|
||||
|
||||
subgraph AG["SAP API Gateway Session"]
|
||||
AS["api_gateway_session (requests.Session)"]
|
||||
AC["Manager Credentials (.env: APIGATEWAY_*)"]
|
||||
AS -.->|Renueva en 401| AC
|
||||
end
|
||||
```
|
||||
|
||||
| Capa | Almacenamiento | Ámbito | Timeout | Renovación |
|
||||
| ------------- | ----------------- | -------- | ------------- | -------------------- |
|
||||
| SAP B1 | `sap_session_store` | Global | Gestionado SAP | Automática al recibir 401 |
|
||||
| Empleado | `active_tokens` | Por empleado | 30 min inactividad | Login manual |
|
||||
| Capa | Almacenamiento | Ámbito | Timeout | Renovación |
|
||||
| -------------- | -------------------- | ------------ | ------------------- | --------------------------- |
|
||||
| SAP Service Layer | `sap_session_store` (`core/sap.py`) | Global | Gestionado SAP | Automática al recibir 401 |
|
||||
| SAP API Gateway | `api_gateway_session` (`core/api_gateway.py`) | Global | Gestionado SAP | Automática al recibir 401 |
|
||||
| Empleado | `active_tokens` / `employee_sessions` (`core/session.py`) | Por empleado | 30 min inactividad | Login manual |
|
||||
|
||||
`core/hana.py` no mantiene sesión: abre una conexión nueva a HANA por
|
||||
petición (`hana_connection()`, context manager) y la cierra al terminar.
|
||||
|
||||
## Estructura de Rutas
|
||||
|
||||
| Método | Ruta | Router | Descripción |
|
||||
| -------- | --------------------------------- | -------------------- | ------------------------ |
|
||||
| `POST` | `/api/Login` | `auth.py` | Autenticar empleado |
|
||||
| `POST` | `/api/Logout` | `auth.py` | Cerrar sesión |
|
||||
| `GET` | `/api/BusinessPartners` | `business_partners` | Listar socios |
|
||||
| `GET` | `/api/BusinessPartners/{code}` | `business_partners` | Obtener socio |
|
||||
| `POST` | `/api/BusinessPartners` | `business_partners` | Crear socio |
|
||||
| `PATCH` | `/api/BusinessPartners/{code}` | `business_partners` | Actualizar socio |
|
||||
| `GET` | `/api/Items` | `items.py` | Listar artículos |
|
||||
| `GET` | `/api/Items/{code}` | `items.py` | Obtener artículo |
|
||||
| `POST` | `/api/Items` | `items.py` | Crear artículo |
|
||||
| `PATCH` | `/api/Items/{code}` | `items.py` | Actualizar artículo |
|
||||
| `GET` | `/api/Invoices` | `sales.py` | Listar facturas |
|
||||
| `GET` | `/api/Invoices/{key}` | `sales.py` | Obtener factura |
|
||||
| `POST` | `/api/Invoices` | `sales.py` | Crear factura |
|
||||
| `PATCH` | `/api/Invoices/{key}` | `sales.py` | Actualizar factura |
|
||||
| `GET` | `/api/Orders` | `sales.py` | Listar pedidos |
|
||||
| `GET` | `/api/Orders/{key}` | `sales.py` | Obtener pedido |
|
||||
| `POST` | `/api/Orders` | `sales.py` | Crear pedido |
|
||||
| `PATCH` | `/api/Orders/{key}` | `sales.py` | Actualizar pedido |
|
||||
| `GET` | `/api/PurchaseOrders` | `purchases.py` | Listar órdenes de compra |
|
||||
| `GET` | `/api/PurchaseOrders/{key}` | `purchases.py` | Obtener orden de compra |
|
||||
| `PATCH` | `/api/PurchaseOrders/{key}` | `purchases.py` | Actualizar orden compra |
|
||||
| `GET` | `/api/PurchaseInvoices` | `purchases.py` | Listar facturas compra |
|
||||
| `GET` | `/api/PurchaseInvoices/{key}` | `purchases.py` | Obtener factura compra |
|
||||
| `PATCH` | `/api/PurchaseInvoices/{key}` | `purchases.py` | Actualizar factura compra|
|
||||
| `GET` | `/api/Currencies` | `catalogs.py` | Listar monedas |
|
||||
| `GET` | `/api/BusinessPartnerGroups` | `catalogs.py` | Listar grupos de socios |
|
||||
| `GET` | `/api/Warehouses` | `catalogs.py` | Listar almacenes |
|
||||
| `GET` | `/api/ItemGroups` | `catalogs.py` | Listar grupos artículos |
|
||||
| `GET` | `/api/SalesTaxCodes` | `catalogs.py` | Listar códigos de IVA |
|
||||
| `GET` | `/api/BatchNumberDetails` | `batch.py` | Listar lotes |
|
||||
| `GET` | `/api/BatchNumberDetails/{id}` | `batch.py` | Obtener lote |
|
||||
| `PATCH` | `/api/BatchNumberDetails/{id}` | `batch.py` | Actualizar lote |
|
||||
| `GET` | `/api/user/menu` | `permissions.py` | Obtener menú permitido |
|
||||
| -------- | --------------------------------- | --------------------- | ------------------------ |
|
||||
| `POST` | `/api/Login` | `auth.py` | Autenticar empleado |
|
||||
| `POST` | `/api/Logout` | `auth.py` | Cerrar sesión |
|
||||
| `GET` | `/api/BusinessPartners` | `business_partners` | Listar socios |
|
||||
| `GET` | `/api/BusinessPartners/{code}` | `business_partners` | Obtener socio |
|
||||
| `POST` | `/api/BusinessPartners` | `business_partners` | Crear socio |
|
||||
| `PATCH` | `/api/BusinessPartners/{code}` | `business_partners` | Actualizar socio |
|
||||
| `GET` | `/api/Items` | `items.py` | Listar artículos |
|
||||
| `GET` | `/api/Items/{code}` | `items.py` | Obtener artículo |
|
||||
| `POST` | `/api/Items` | `items.py` | Crear artículo |
|
||||
| `PATCH` | `/api/Items/{code}` | `items.py` | Actualizar artículo |
|
||||
| `GET/POST/PATCH` | `/api/Invoices[/{key}]` | `sales.py` | Facturas de venta |
|
||||
| `GET/POST/PATCH` | `/api/Orders[/{key}]` | `sales.py` | Pedidos de venta |
|
||||
| `GET/POST/PATCH` | `/api/DeliveryNotes[/{key}]` | `sales.py` | Guías de remisión de venta |
|
||||
| `GET/PATCH` | `/api/PurchaseOrders[/{key}]` | `purchases.py` | Órdenes de compra |
|
||||
| `GET/PATCH` | `/api/PurchaseDeliveryNotes[/{key}]` | `purchases.py` | Guías de recepción de compra |
|
||||
| `GET/PATCH` | `/api/PurchaseInvoices[/{key}]` | `purchases.py` | Facturas de compra |
|
||||
| `GET` | `/api/Currencies` | `catalogs.py` | Listar monedas |
|
||||
| `GET` | `/api/BusinessPartnerGroups` | `catalogs.py` | Listar grupos de socios |
|
||||
| `GET` | `/api/ItemGroups` | `catalogs.py` | Listar grupos artículos |
|
||||
| `GET` | `/api/Warehouses` | `catalogs.py` | Listar almacenes |
|
||||
| `GET` | `/api/SalesTaxCodes` | `catalogs.py` | Listar códigos de IVA |
|
||||
| `GET` | `/api/FactoringIndicators` | `catalogs.py` | Listar indicadores de factoring |
|
||||
| `GET` | `/api/Sunat/BusinessPartners/DocumentType` | `catalogs.py` | Tipos de documento SUNAT (campo de usuario `EXX_TIPODOCU`) |
|
||||
| `GET` | `/api/BatchNumberDetails` | `batch.py` | Listar lotes |
|
||||
| `GET` | `/api/BatchNumberDetails/{id}` | `batch.py` | Obtener lote |
|
||||
| `PATCH` | `/api/BatchNumberDetails/{id}` | `batch.py` | Actualizar lote |
|
||||
| `GET` | `/api/user/menu` | `permissions.py` | Obtener menú permitido |
|
||||
| `GET` | `/api/apigateway/reports` | `apigateway.py` | Listar reportes Crystal Reports autorizados |
|
||||
| `GET` | `/api/apigateway/reports/{docCode}/parameters` | `apigateway.py` | Parámetros de un reporte Crystal Reports |
|
||||
| `POST` | `/api/apigateway/reports/{docCode}/export` | `apigateway.py` | Exportar reporte Crystal Reports a PDF |
|
||||
| `POST` | `/api/reports/stickers` | `reports.py` | Generar PDF/HTML de stickers de guía de remisión (consulta directa a HANA + plantilla Jinja + WeasyPrint) |
|
||||
|
||||
Los recursos servidos por `core/crud_router.py` (todos los de la tabla salvo
|
||||
`auth`, `permissions`, `apigateway` y `reports`) soportan además `skip`,
|
||||
`top`, `fields` y `filter` como query params, y algunos (`Invoices`,
|
||||
`Orders`, `DeliveryNotes`, `PurchaseOrders`, `PurchaseDeliveryNotes`,
|
||||
`PurchaseInvoices`, `BatchNumberDetails`) también `orderby`. Ver
|
||||
[API Reference](api-reference.md) para el detalle completo por endpoint.
|
||||
|
||||
## Dependencias del Proyecto
|
||||
|
||||
@@ -160,24 +204,44 @@ graph TD
|
||||
main --> catalogs
|
||||
main --> batch
|
||||
main --> permissions
|
||||
main --> apigateway_router[apigateway]
|
||||
main --> reports_router[reports]
|
||||
|
||||
auth --> sap_core
|
||||
auth --> session_core
|
||||
business_partners --> sap_core
|
||||
items --> sap_core
|
||||
sales --> sap_core
|
||||
purchases --> sap_core
|
||||
business_partners --> crud_router
|
||||
items --> crud_router
|
||||
sales --> crud_router
|
||||
purchases --> crud_router
|
||||
catalogs --> crud_router
|
||||
catalogs --> sap_core
|
||||
batch --> sap_core
|
||||
batch --> crud_router
|
||||
crud_router --> sap_core
|
||||
permissions --> menu_json[menu_permissions.json]
|
||||
apigateway_router --> api_gateway_core[api_gateway]
|
||||
reports_router --> hana_core[hana]
|
||||
reports_router --> templates_reports[templates/reports/*.html]
|
||||
```
|
||||
|
||||
## Configuración por Entorno
|
||||
|
||||
La aplicación carga siempre `.env` (un solo archivo, sin sufijos de entorno).
|
||||
`.env.aberio` se conserva como plantilla de referencia; para desplegar contra
|
||||
ese entorno, copiar su contenido dentro de `.env`.
|
||||
La aplicación carga siempre `mini-backend/.env` (un solo archivo por
|
||||
despliegue, sin sufijos de entorno) mediante `load_dotenv`. Ese archivo
|
||||
**no se versiona** (está en `.gitignore`); la plantilla de referencia con
|
||||
todas las variables necesarias y valores de ejemplo es
|
||||
[`mini-backend/.env.example`](../.env.example).
|
||||
|
||||
| Archivo | URL SAP | Company DB |
|
||||
| ------------------ | ------------------------------- | ----------------------- |
|
||||
| `.env` | `192.168.1.175:50000` | `SBO_PRUEBA_TAGUM_22` |
|
||||
| `.env.aberio` | `192.168.9.3:50000` | `PRUEBA_20260609` |
|
||||
Grupos de variables:
|
||||
|
||||
| Grupo | Variables | Usado por |
|
||||
| --------------------- | -------------------------------------------------------------------------- | --------------------- |
|
||||
| SAP Service Layer | `SAPB1_SL_URL`, `SAPB1_SL_USER`, `SAPB1_SL_PASSWORD`, `SAPB1_SL_COMPANYDB`, `SAPB1_SL_TIMEOUT`, `SAPB1_SL_VERIFY_SSL` | `core/sap.py`, `routers/auth.py` |
|
||||
| SAP API Gateway | `APIGATEWAY_URL`, `APIGATEWAY_USER`, `APIGATEWAY_PASSWORD`, `APIGATEWAY_COMPANYDB`, `APIGATEWAY_DBINSTANCE`, `APIGATEWAY_TIMEOUT` | `core/api_gateway.py` |
|
||||
| HANA (acceso directo) | `HANA_HOST`, `HANA_PORT`, `HANA_USER`, `HANA_PASSWORD`, `HANA_SCHEMA` | `core/hana.py`, `routers/reports.py` |
|
||||
| Logging | `LOG_ENVIRONMENT` (`production` = JSON estructurado, cualquier otro valor = texto legible en consola) | `main.py`, `logger_config.py` |
|
||||
|
||||
Al desplegar para un cliente nuevo (ver flujo de branches por cliente en
|
||||
`CLAUDE.md` de la raíz), se copia `.env.example` a `.env` dentro de
|
||||
`mini-backend/` y se completa con las credenciales reales de SAP Business
|
||||
One / HANA de ese cliente. Las credenciales reales nunca deben
|
||||
documentarse ni commitearse.
|
||||
@@ -11,17 +11,17 @@ sequenceDiagram
|
||||
participant SAP as SAP Service Layer
|
||||
|
||||
F->>API: POST /api/Login\n{Username, Password}
|
||||
Note over F,API: Username = emp_id\nPassword = LastName
|
||||
Note over F,API: Username = webUser (U_WEB_USER)\nPassword = U_WEB_PASS
|
||||
|
||||
API->>SAP_C: forward_to_sapb1(GET, "EmployeesInfo({empId})")
|
||||
SAP_C->>SAP: GET /b1s/v1/EmployeesInfo({empId})
|
||||
SAP-->>SAP_C: {FirstName, LastName, ...}
|
||||
SAP_C-->>API: Employee data
|
||||
API->>SAP_C: forward_to_sapb1(GET, "EmployeesInfo")\n$filter=U_WEB_USER eq '{webUser}'
|
||||
SAP_C->>SAP: GET /b1s/v1/EmployeesInfo?$filter=...
|
||||
SAP-->>SAP_C: {EmployeeID, FirstName, LastName, Active, U_WEB_PASS, ...}
|
||||
SAP_C-->>API: Employee data (o lista vacía)
|
||||
|
||||
API->>API: ¿Password == LastName?
|
||||
alt Coincide
|
||||
API->>S: create_employee_session(emp_id, first_name, last_name)
|
||||
S->>S: Genera token_urlsafe(32)
|
||||
API->>API: ¿Existe empleado?\n¿Active == "tYES"?\n¿U_WEB_PASS == Password?
|
||||
alt Todo válido
|
||||
API->>S: create_employee_session(webUser, empId, first_name, last_name)
|
||||
S->>S: Genera token_urlsafe(32)\n(invalida token anterior del mismo webUser)
|
||||
S-->>API: token
|
||||
|
||||
API->>SAP_C: ensure_sap_session()
|
||||
@@ -29,9 +29,9 @@ sequenceDiagram
|
||||
SAP-->>SAP_C: B1SESSION cookie
|
||||
SAP_C-->>API: OK
|
||||
|
||||
API-->>F: 200 {message, token, username}
|
||||
else No coincide
|
||||
API-->>F: 401 Invalid credentials
|
||||
API-->>F: 200 {message, token, username: FirstName, company: CompanyDB}
|
||||
else No existe / inactivo / password no coincide
|
||||
API-->>F: 401 (mensaje según el caso: "Usuario no registrado",\n"Usuario inactivo" o "Usuario o contraseña incorrectos")
|
||||
end
|
||||
```
|
||||
|
||||
@@ -68,8 +68,8 @@ sequenceDiagram
|
||||
|
||||
alt Token válido
|
||||
S->>S: Actualiza last_activity
|
||||
S-->>MW: emp_id
|
||||
MW->>MW: Adjunta emp_id a request.state
|
||||
S-->>MW: webUser
|
||||
MW->>MW: set_current_user(webUser, full_name, emp_id)\nrequest.state.webUser = webUser
|
||||
MW->>R: Continúa al router
|
||||
R-->>F: 200 Response
|
||||
else Token expirado (>30 min)
|
||||
|
||||
+17
-12
@@ -1,6 +1,6 @@
|
||||
# Backend SN — Documentación del Proyecto
|
||||
# Backend — Documentación del Proyecto
|
||||
|
||||
Backend de integración entre una aplicación frontend (Next.js) y **SAP Business One Service Layer**. Actúa como proxy/middleware autenticando empleados, gestionando sesiones y enrutando operaciones CRUD hacia SAP.
|
||||
Backend de integración entre el frontend `mini-frontend` (React 19 + TypeScript + Vite) y **SAP Business One Service Layer**. Actúa como proxy/middleware autenticando empleados, gestionando sesiones y enrutando operaciones CRUD hacia SAP. Este proyecto vive junto al frontend dentro del monorepo `mini-erp` (ver `CLAUDE.md` en la raíz del repo).
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
@@ -17,29 +17,34 @@ Backend de integración entre una aplicación frontend (Next.js) y **SAP Busines
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Frontend"
|
||||
F[App SN - Next.js]
|
||||
F[mini-frontend - React + Vite]
|
||||
end
|
||||
|
||||
subgraph "Backend SN - FastAPI"
|
||||
subgraph "Backend - FastAPI"
|
||||
API[API Proxy\n:8000]
|
||||
end
|
||||
|
||||
subgraph "SAP Business One"
|
||||
SL[Service Layer\nREST API]
|
||||
DB[(SAP DB\nSBO_PRUEBA_*)]
|
||||
AG[API Gateway\nCrystal Reports]
|
||||
DB[(SAP DB / HANA)]
|
||||
end
|
||||
|
||||
F -->|HTTP /api/*| API
|
||||
API -->|HTTP /b1s/v1| SL
|
||||
API -->|HTTP rs/v1/*| AG
|
||||
API -->|SQL directo| DB
|
||||
SL --> DB
|
||||
AG --> DB
|
||||
```
|
||||
|
||||
## Documentos
|
||||
|
||||
| Documento | Descripción |
|
||||
| -------------------------------------------- | -------------------------------------------- |
|
||||
| [Arquitectura](architecture.md) | Componentes, dependencias, middlewares, APIs |
|
||||
| [Flujo de Autenticación](auth-flow.md) | Login, logout, sesiones, middleware |
|
||||
| [Proxy SAP](sap-proxy.md) | Enrutamiento a SAP, re-autenticación, OData |
|
||||
| [API Reference](api-reference.md) | Todos los endpoints, ejemplos request/response |
|
||||
| [Visualización de logs](logs-viewing.md) | Cómo leer `logs/backend.log` con `jq`/`lnav` |
|
||||
| Documento | Descripción |
|
||||
| ---------------------------------------- | ---------------------------------------------- |
|
||||
| [Arquitectura](architecture.md) | Componentes, dependencias, middlewares, APIs |
|
||||
| [Flujo de Autenticación](auth-flow.md) | Login, logout, sesiones, middleware |
|
||||
| [Proxy SAP](sap-proxy.md) | Enrutamiento a SAP, re-autenticación, OData |
|
||||
| [API Reference](api-reference.md) | Todos los endpoints, ejemplos request/response |
|
||||
| [Diseño de logging](logging-plan.md) | Contexto de usuario/request, formato JSON |
|
||||
| [Visualización de logs](logs-viewing.md) | Cómo leer `logs/backend.log` con `jq`/`lnav` |
|
||||
@@ -1,175 +1,84 @@
|
||||
# Plan: logging real por sesión de usuario
|
||||
# Diseño de Logging
|
||||
|
||||
## Problema actual (diagnosticado)
|
||||
|
||||
- `main.py:32` fuerza `setup_logging(environment="debug")` siempre — el modo
|
||||
`"production"` (JSON estructurado) de `logger_config.py` nunca se activa,
|
||||
sin importar el entorno real de despliegue.
|
||||
- `core/sap.py`, `core/api_gateway.py` y `routers/auth.py` usan `print()` en
|
||||
vez del `logger` real. Esto significa:
|
||||
- No queda nada en el archivo rotativo `logs/backend.log` (solo en la
|
||||
consola / `docker compose logs`).
|
||||
- No tiene formato JSON buscable, ni `request_id` de correlación.
|
||||
- **Grave**: `routers/auth.py` imprime la contraseña (apellido) en texto
|
||||
plano dos veces:
|
||||
- línea ~27: `print(f"Contraseña (Apellido) recibida: {empLastName}")`
|
||||
- línea ~45: `print(f"Apellido en SAP: {employee_data.get('LastName')}")`
|
||||
> Nota: este documento describía originalmente un plan pendiente. Ya está
|
||||
> implementado tal como se describe abajo; se conserva el nombre del
|
||||
> archivo por los enlaces existentes desde `index.md`.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Además de arreglar lo anterior, dejar un log **por sesión de usuario
|
||||
conectado**: que cada línea de log quede etiquetada con qué empleado
|
||||
(`emp_id` + nombre) hizo qué acción (`GET /Items`, `POST /BusinessPartners`,
|
||||
etc.), en JSON, para poder cargarlo después en un monitor/dashboard y buscar
|
||||
acciones por usuario.
|
||||
Cada línea de log queda etiquetada automáticamente con el `request_id` de
|
||||
la petición HTTP y con el empleado conectado que la originó (`webUser`,
|
||||
`emp_id`, `full_name`), en formato JSON, sin tener que pasar esos datos a
|
||||
mano en cada llamada a `logger.info/warning/error`.
|
||||
|
||||
## Diseño
|
||||
## Piezas
|
||||
|
||||
`context.py` ya tiene un `ContextVar` para el `request_id` que se inyecta
|
||||
automáticamente en cada log a través de `logger_config.JsonFormatter`. Se
|
||||
replica el mismo patrón para el usuario conectado, así **todo log queda
|
||||
etiquetado automáticamente sin tocar cada llamada individual**.
|
||||
### `context.py`
|
||||
|
||||
### 1. `context.py`
|
||||
Dos `ContextVar` por request (aisladas entre corrutinas concurrentes de
|
||||
FastAPI):
|
||||
|
||||
Agregar un `ContextVar` para el usuario actual, mismo patrón que
|
||||
`request_id_ctx`:
|
||||
- `request_id_ctx` — expuesta via `get_request_id()` / `set_request_id()`.
|
||||
- `current_user_ctx` — dict `{"webUser", "emp_id", "full_name"}`, expuesta
|
||||
via `get_current_user()` / `set_current_user(webUser, full_name="", emp_id="-")`.
|
||||
Por defecto ambas quedan en `"-"` cuando no hay usuario autenticado
|
||||
(por ejemplo, en el propio `POST /api/Login` antes de validar credenciales).
|
||||
|
||||
```python
|
||||
current_user_ctx: ContextVar[dict] = ContextVar(
|
||||
"current_user", default={"emp_id": "-", "username": "-"}
|
||||
)
|
||||
### `main.py`
|
||||
|
||||
def get_current_user() -> dict:
|
||||
return current_user_ctx.get()
|
||||
- `correlation_id_middleware` llama a `set_request_id()` con el header
|
||||
`X-Request-ID` entrante o un UUID generado si no viene.
|
||||
- `session_middleware`, tras validar el token con
|
||||
`validate_and_refresh_session()`, llama a
|
||||
`set_current_user(webUser, get_employee_name(webUser), get_emp_id(webUser))`
|
||||
para que el resto de la petición (incluyendo las llamadas a
|
||||
`forward_to_sapb1`) quede etiquetada con ese usuario.
|
||||
- `routers/auth.py` también llama a `set_current_user(...)` en un login
|
||||
exitoso, porque `/api/Login` no pasa por el `session_middleware`
|
||||
(todavía no hay token) y de otro modo su propio log quedaría sin usuario.
|
||||
- `LOG_ENVIRONMENT` (variable de entorno, default `"production"`) decide el
|
||||
formato de salida en `setup_logging()`.
|
||||
|
||||
def set_current_user(emp_id: str, username: str):
|
||||
current_user_ctx.set({"emp_id": emp_id, "username": username})
|
||||
```
|
||||
### `logger_config.py` — `JsonFormatter`
|
||||
|
||||
### 2. `core/session.py`
|
||||
|
||||
Agregar un helper que devuelva "Nombre Apellido" a partir de `emp_id`,
|
||||
leyendo el dict `employee_sessions` que ya existe:
|
||||
|
||||
```python
|
||||
def get_employee_name(emp_id: str) -> str:
|
||||
session = employee_sessions.get(emp_id)
|
||||
if not session:
|
||||
return "-"
|
||||
return f"{session.get('first_name', '')} {session.get('last_name', '')}".strip()
|
||||
```
|
||||
|
||||
### 3. `main.py`
|
||||
|
||||
En `session_middleware`, justo después de validar el token y obtener
|
||||
`emp_id` (donde hoy se hace `request.state.emp_id = emp_id`), setear el
|
||||
contexto de usuario:
|
||||
|
||||
```python
|
||||
from core.session import validate_and_refresh_session, get_employee_name
|
||||
from context import set_current_user
|
||||
...
|
||||
emp_id = validate_and_refresh_session(token)
|
||||
if not emp_id:
|
||||
...
|
||||
set_current_user(emp_id, get_employee_name(emp_id))
|
||||
request.state.emp_id = emp_id
|
||||
```
|
||||
|
||||
También en este archivo: dejar de forzar `"debug"` en `setup_logging`,
|
||||
leerlo de una variable de entorno nueva (por defecto `"production"`, para
|
||||
que Docker quede bien sin configurar nada extra):
|
||||
|
||||
```python
|
||||
LOG_ENVIRONMENT = os.getenv("LOG_ENVIRONMENT", "production")
|
||||
...
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
setup_logging(environment=LOG_ENVIRONMENT)
|
||||
yield
|
||||
```
|
||||
|
||||
### 4. `logger_config.py`
|
||||
|
||||
Extender `JsonFormatter` para incluir `emp_id`/`username` desde el
|
||||
contexto en cada línea, y soportar campos extra estructurados
|
||||
(`http_method`, `endpoint`, `status_code`) cuando el log los trae vía
|
||||
`extra={...}`:
|
||||
|
||||
```python
|
||||
from context import get_request_id, get_current_user
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
EXTRA_FIELDS = ("http_method", "endpoint", "status_code")
|
||||
|
||||
def format(self, record):
|
||||
user = get_current_user()
|
||||
log_record = {
|
||||
"timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
|
||||
"level": record.levelname,
|
||||
"request_id": get_request_id(),
|
||||
"emp_id": user["emp_id"],
|
||||
"username": user["username"],
|
||||
"message": record.getMessage(),
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
"line": record.lineno,
|
||||
}
|
||||
for field in self.EXTRA_FIELDS:
|
||||
if hasattr(record, field):
|
||||
log_record[field] = getattr(record, field)
|
||||
if record.exc_info:
|
||||
log_record["exception"] = self.formatException(record.exc_info)
|
||||
return json.dumps(log_record)
|
||||
```
|
||||
|
||||
Resultado esperado por línea, por ejemplo:
|
||||
En modo `"production"` (default), cada línea de log —tanto en consola como
|
||||
en `logs/backend.log`— es un JSON con esta forma:
|
||||
|
||||
```json
|
||||
{"timestamp": "2026-08-14T...", "level": "INFO", "request_id": "...", "emp_id": "11", "username": "Juan Pérez", "http_method": "GET", "endpoint": "Items", "status_code": 200, "message": "Solicitud SAP completada", "module": "sap", "function": "forward_to_sapb1", "line": 137}
|
||||
{"timestamp": "2026-08-14T15:32:10.123456+00:00", "level": "INFO", "request_id": "3f2a...", "webUser": "jgarcia", "emp_id": "11", "full_name": "Juan García", "http_method": "GET", "endpoint": "Items", "status_code": 200, "message": "[200] GET Items", "module": "sap", "function": "forward_to_sapb1", "line": 141}
|
||||
```
|
||||
|
||||
### 5. `core/sap.py` / `core/api_gateway.py`
|
||||
Campos siempre presentes: `timestamp`, `level`, `request_id`, `webUser`,
|
||||
`emp_id`, `full_name`, `message`, `module`, `function`, `line`. Campos
|
||||
opcionales (`http_method`, `endpoint`, `status_code`) se agregan solo si el
|
||||
`logger.info/warning/error(...)` que generó la línea los pasó vía
|
||||
`extra={...}` — este es el patrón que usan `core/sap.py`,
|
||||
`core/api_gateway.py` y `routers/reports.py` para dejar trazabilidad de
|
||||
cada llamada saliente. Si el log viene de una excepción (`exc_info`), se
|
||||
agrega también `exception` con el traceback formateado.
|
||||
|
||||
Reemplazar cada `print(...)` por `logger.info/warning/error(...)`,
|
||||
agregando `extra={"http_method": method, "endpoint": endpoint_path,
|
||||
"status_code": response.status_code}` en las líneas de request/respuesta,
|
||||
para que queden como campos estructurados y no solo texto libre. Quitar la
|
||||
variable `I = " "` (indentación para prints, ya no hace falta) y los
|
||||
emojis (el nivel del log ya indica severidad).
|
||||
En cualquier otro valor de `LOG_ENVIRONMENT` (por ejemplo `"development"`),
|
||||
la consola usa un formato de texto plano legible
|
||||
(`%(asctime)s [%(levelname)s] %(module)s.%(funcName)s:%(lineno)d - %(message)s`)
|
||||
en vez de JSON — pensado para desarrollo local, no para buscar en el
|
||||
archivo. El archivo `logs/backend.log` (rotativo, 5 MB x 3 backups vía
|
||||
`RotatingFileHandler`) siempre se escribe en JSON sin importar el entorno.
|
||||
|
||||
### 6. `routers/auth.py`
|
||||
## Qué NO se loguea
|
||||
|
||||
- Agregar `logger = logging.getLogger(__name__)`.
|
||||
- Reemplazar todos los `print()` por `logger.info/warning/error(...)`.
|
||||
- **No loguear nunca `empLastName` ni `employee_data.get('LastName')`** —
|
||||
solo el `emp_id` y si el login fue exitoso o no.
|
||||
- En cuanto se confirma el empleado (login exitoso), llamar
|
||||
`set_current_user(empId, f"{first_name} {last_name}")` para que el
|
||||
propio log del login quede etiquetado también (el middleware no aplica
|
||||
a `/api/Login` porque no requiere token todavía).
|
||||
`routers/auth.py` nunca escribe `Password` / `U_WEB_PASS` en los logs, ni en
|
||||
éxito ni en fallo de login — solo el `webUser`, el `EmployeeID` y si el
|
||||
intento fue exitoso o el motivo del rechazo (`"Usuario no registrado"`,
|
||||
`"Usuario inactivo"`, contraseña incorrecta). Al auditar un cambio en el
|
||||
flujo de login, verificar que esta regla se mantenga.
|
||||
|
||||
### 7. `docker-compose.yml`
|
||||
|
||||
Agregar volumen para que el log persista en el host aunque se reconstruya
|
||||
el contenedor (si no, se perdería cada `--build`, igual que pasaba con
|
||||
`.env` antes de agregar su volumen):
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
volumes:
|
||||
- ./mini-backend/menu_permissions.json:/app/menu_permissions.json:ro
|
||||
- ./mini-backend/logs:/app/logs
|
||||
```
|
||||
|
||||
## Verificación al implementar
|
||||
## Verificación
|
||||
|
||||
1. Levantar el backend (local o Docker).
|
||||
2. Loguearse con un usuario de prueba y hacer un par de llamadas reales
|
||||
(GET a algún catálogo, POST/PATCH a algo).
|
||||
3. Revisar `logs/backend.log`: cada línea debe traer `emp_id`, `username`,
|
||||
`http_method`, `endpoint`, `status_code` correctos.
|
||||
3. Revisar `logs/backend.log`: cada línea debe traer `webUser`, `emp_id`,
|
||||
`full_name`, y las líneas de proxy SAP también `http_method`,
|
||||
`endpoint`, `status_code`.
|
||||
4. `grep` de la contraseña de prueba sobre todo `logs/backend.log` (y la
|
||||
salida de consola) para confirmar que **no aparece en ningún lado**.
|
||||
5. Confirmar que `docker compose build` sigue funcionando sin errores.
|
||||
@@ -1,11 +1,11 @@
|
||||
# Visualización de logs
|
||||
|
||||
El backend escribe en `logs/backend.log` una línea JSON por evento (ver
|
||||
[logging-plan.md](logging-plan.md)), con campos como `emp_id`, `username`,
|
||||
`http_method`, `endpoint` y `status_code`. Es el formato correcto para
|
||||
buscar/cargar en un dashboard, pero incómodo de leer a simple vista con
|
||||
`cat`/`tail`. Estas son las dos formas recomendadas de verlo cómodo desde la
|
||||
terminal.
|
||||
[logging-plan.md](logging-plan.md)), con campos como `webUser`, `emp_id`,
|
||||
`full_name`, `http_method`, `endpoint` y `status_code`. Es el formato
|
||||
correcto para buscar/cargar en un dashboard, pero incómodo de leer a simple
|
||||
vista con `cat`/`tail`. Estas son las dos formas recomendadas de verlo
|
||||
cómodo desde la terminal.
|
||||
|
||||
## `jq` — filtrar y colorear en vivo
|
||||
|
||||
@@ -15,8 +15,8 @@ Instalación: `brew install jq`
|
||||
# seguir el log en vivo, con formato e indentación
|
||||
tail -f logs/backend.log | jq .
|
||||
|
||||
# solo las líneas de un empleado puntual
|
||||
tail -f logs/backend.log | jq 'select(.emp_id == 10)'
|
||||
# solo las líneas de un empleado puntual (emp_id es string, ej. "11")
|
||||
tail -f logs/backend.log | jq 'select(.emp_id == "11")'
|
||||
|
||||
# solo errores/warnings
|
||||
tail -f logs/backend.log | jq 'select(.level == "ERROR" or .level == "WARNING")'
|
||||
@@ -25,7 +25,7 @@ tail -f logs/backend.log | jq 'select(.level == "ERROR" or .level == "WARNING")'
|
||||
tail -f logs/backend.log | jq 'select(.endpoint == "Items")'
|
||||
|
||||
# ver solo unos campos, en una línea por evento
|
||||
tail -f logs/backend.log | jq -c '{ts: .timestamp, user: .username, ep: .endpoint, status: .status_code}'
|
||||
tail -f logs/backend.log | jq -c '{ts: .timestamp, user: .full_name, ep: .endpoint, status: .status_code}'
|
||||
```
|
||||
|
||||
## `lnav` — visor interactivo
|
||||
@@ -41,9 +41,9 @@ lnav logs/backend.log
|
||||
- colores por nivel (`INFO`/`WARNING`/`ERROR`).
|
||||
- búsqueda de texto con `/`.
|
||||
- filtros en vivo con `:filter-in` / `:filter-out` (por ejemplo
|
||||
`:filter-in emp_id.:.10` para quedarte solo con el empleado 10).
|
||||
`:filter-in emp_id.:.11` para quedarte solo con el empleado 11).
|
||||
- queries tipo SQL sobre los campos del log con `;` (por ejemplo
|
||||
`;SELECT emp_id, username, endpoint, status_code FROM backend_log WHERE level = 'ERROR'`).
|
||||
`;SELECT emp_id, full_name, endpoint, status_code FROM backend_log WHERE level = 'ERROR'`).
|
||||
|
||||
Es la opción más cómoda para explorar el log completo o investigar un
|
||||
incidente puntual sin escribir comandos `jq` largos.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Función Central: `forward_to_sapb1`
|
||||
|
||||
Todas las operaciones CRUD pasan por esta función en `core/sap.py`, que actúa como proxy hacia SAP Service Layer.
|
||||
Todas las operaciones CRUD pasan por esta función en `core/sap.py`, que actúa como proxy hacia SAP Service Layer. La mayoría de los routers (`business_partners`, `items`, `sales`, `purchases`, `batch`, parte de `catalogs`) no la llaman directamente: pasan por `core/crud_router.py`, una factory que arma el `list/get/create/update` de un recurso OData y delega en `forward_to_sapb1` (ver [Arquitectura](architecture.md)). El proxy hacia el API Gateway de Crystal Reports (`core/api_gateway.py`, usado por `routers/apigateway.py`) sigue el mismo patrón de sesión-global + reintento en 401, mostrado más abajo bajo "Otros proxies".
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -116,3 +116,15 @@ sequenceDiagram
|
||||
| Error de red | Lanza `ConnectionError` |
|
||||
| Reintento también falla | Lanza `HTTPException` con el mensaje de error SAP |
|
||||
| SAP no disponible | `HTTPException(500)` |
|
||||
|
||||
## Otros proxies (fuera de Service Layer)
|
||||
|
||||
| Proxy | Módulo | Sesión | Consumido por |
|
||||
| ----- | ------ | ------ | -------------- |
|
||||
| API Gateway (Crystal Reports, `rs/v1/*`) | `core/api_gateway.py` (`forward_to_api_gateway`) | `requests.Session` con cookies, global, se re-loguea en 401 igual que el Service Layer | `routers/apigateway.py` |
|
||||
| HANA (SQL directo) | `core/hana.py` (`hana_connection`) | Sin sesión persistente: abre y cierra una conexión `hdbcli` por request | `routers/reports.py` |
|
||||
|
||||
`core/hana.py` no pasa por `forward_to_sapb1` ni maneja OData: se usa para
|
||||
reportes que necesitan una consulta SQL puntual contra HANA (por ejemplo,
|
||||
armar el detalle de una guía de remisión para imprimir stickers) en vez de
|
||||
paginar un recurso del Service Layer.
|
||||
Reference in new issue
Block a user