primer commit
This commit is contained in:
1 parent
ab3c613165
commit
b2fb4e2d0a
18 files changed
+1291
-277
No files matched your search
-15
@@ -22,21 +22,6 @@ mini-frontend/certs/
|
|||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
# Logs genéricos
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Root env
|
# Root env
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# mini-erp
|
||||||
|
|
||||||
|
Integración web para SAP Business One (Service Layer). Es un monorepo con dos proyectos que **siempre se instalan y despliegan juntos**, orquestados por `docker-compose.yml` en la raíz.
|
||||||
|
|
||||||
|
## Estructura
|
||||||
|
|
||||||
|
- `mini-backend/` — API FastAPI (Python) que hace de puente hacia SAP Business One Service Layer (HANA).
|
||||||
|
- `main.py` — entrypoint, registro de routers y middlewares.
|
||||||
|
- `routers/` — endpoints por dominio: `auth`, `business_partners`, `items`, `sales`, `purchases`, `catalogs`, `batch`, `permissions`, `apigateway`, `reports`.
|
||||||
|
- `core/` — lógica compartida: `sap.py` / `hana.py` (integración SAP/HANA), `session.py` (sesión y refresco), `api_gateway.py`, `crud_router.py`.
|
||||||
|
- `menu_permissions.json` — se lee del disco en cada request (no se cachea), por lo que editarlo en el host aplica el cambio sin reiniciar el contenedor.
|
||||||
|
- `mini-frontend/` — SPA en React 19 + TypeScript + Vite, Tailwind CSS 4, React Router 7.
|
||||||
|
- `src/views`, `src/components`, `src/layout`, `src/hooks`, `src/config`, `src/types`, `src/utils`.
|
||||||
|
|
||||||
|
## Levantar el entorno
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
- Backend expuesto en `:8000`, frontend en `:80`.
|
||||||
|
- Variables de entorno vía `.env` (backend) y build args `VITE_API_URL`/`TZ` (frontend). Ver `.env.example`.
|
||||||
|
|
||||||
|
## Flujo de trabajo por cliente (branches)
|
||||||
|
|
||||||
|
Este repo es la base genérica del ERP. El flujo real de trabajo es:
|
||||||
|
|
||||||
|
1. `main` contiene la versión base/genérica de `mini-backend` + `mini-frontend`, sin personalizaciones de ningún cliente.
|
||||||
|
2. Cuando un cliente solicita el sistema, se crea una branch dedicada para ese cliente (convención: `cliente/<nombre-cliente>`).
|
||||||
|
3. Todas las personalizaciones, agregados y fixes específicos de ese cliente se desarrollan y versionan **dentro de esa branch**, nunca directamente en `main`.
|
||||||
|
4. Los fixes o cambios hechos en la branch de un cliente **no deben mezclarse** con `main` ni con la branch de otro cliente, salvo que se trate explícitamente de una mejora genérica que se decide llevar a `main` (backport consciente, no automático).
|
||||||
|
5. Antes de crear una branch nueva, mergear a `main`, o hacer merge/backport entre branches de clientes, confirmar con el usuario — son operaciones que afectan el versionamiento compartido y no deben asumirse.
|
||||||
|
|
||||||
|
### Implicancias para el trabajo diario
|
||||||
|
|
||||||
|
- Si no se especifica lo contrario, asumir que el trabajo en curso es sobre la branch actual (base o de un cliente puntual), no sobre `main`.
|
||||||
|
- No hacer `git push`, merge hacia `main`, ni cherry-pick entre branches de clientes sin confirmación explícita.
|
||||||
|
- Si se detecta que se está en `main` y se pide una personalización que suena específica de un cliente, preguntar si corresponde crear/usar una branch de cliente en vez de modificar `main` directamente.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Variables del backend (FastAPI). Copiar este archivo a ".env" en esta
|
||||||
|
# misma carpeta y completar con las credenciales reales del cliente/entorno.
|
||||||
|
# NUNCA commitear el ".env" real (ya está en .gitignore): tiene contraseñas
|
||||||
|
# de SAP Business One y HANA en texto plano.
|
||||||
|
|
||||||
|
# --- SAP Business One Service Layer ---
|
||||||
|
SAPB1_SL_URL=https://<host-sap>:50000/b1s/v1
|
||||||
|
SAPB1_SL_USER=manager
|
||||||
|
SAPB1_SL_PASSWORD=<password>
|
||||||
|
SAPB1_SL_COMPANYDB=<NOMBRE_COMPANYDB>
|
||||||
|
SAPB1_SL_TIMEOUT=30.0
|
||||||
|
# true/false: en redes internas con certificado autofirmado suele ir en false.
|
||||||
|
SAPB1_SL_VERIFY_SSL=false
|
||||||
|
|
||||||
|
# --- SAP Business One API Gateway (Service Layer B1if / DI-API) ---
|
||||||
|
APIGATEWAY_URL=https://<host-sap>:60000
|
||||||
|
APIGATEWAY_USER=manager
|
||||||
|
APIGATEWAY_PASSWORD=<password>
|
||||||
|
APIGATEWAY_COMPANYDB=<NOMBRE_COMPANYDB>
|
||||||
|
APIGATEWAY_DBINSTANCE=NDB@<host-hana>:30013
|
||||||
|
APIGATEWAY_TIMEOUT=30.0
|
||||||
|
|
||||||
|
# --- HANA (acceso directo a la base, ej. para reportes) ---
|
||||||
|
HANA_HOST=<host-hana>
|
||||||
|
HANA_PORT=30015
|
||||||
|
HANA_USER=SYSTEM
|
||||||
|
HANA_PASSWORD=<password>
|
||||||
|
HANA_SCHEMA=<NOMBRE_COMPANYDB>
|
||||||
|
|
||||||
|
# --- Logging ---
|
||||||
|
# production | development (ver logger_config.py)
|
||||||
|
LOG_ENVIRONMENT=production
|
||||||
@@ -39,14 +39,17 @@ Todas las respuestas de error siguen el mismo formato:
|
|||||||
|
|
||||||
### `POST /api/Login`
|
### `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:**
|
**Request body:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Username": "123",
|
"Username": "jgarcia",
|
||||||
"Password": "Garcia"
|
"Password": "********"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,7 +59,8 @@ Inicia sesión de empleado. Valida contra SAP EmployeesInfo usando el apellido c
|
|||||||
{
|
{
|
||||||
"message": "Login exitoso",
|
"message": "Login exitoso",
|
||||||
"token": "aB3x...token_urlsafe_32_chars...",
|
"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`
|
### `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
|
## Compras
|
||||||
|
|
||||||
### Órdenes de Compra
|
### Órdenes de Compra
|
||||||
@@ -372,6 +410,18 @@ Actualiza un pedido. Usa header `B1S-ReplaceCollectionsOnPatch: true`.
|
|||||||
|
|
||||||
Lista paginada. Mismos parámetros que Invoices/Orders.
|
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}`
|
#### `GET /api/PurchaseOrders/{key}`
|
||||||
|
|
||||||
Obtiene una orden de compra por `DocEntry`.
|
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.
|
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}`
|
#### `GET /api/PurchaseInvoices/{key}`
|
||||||
|
|
||||||
Obtiene una factura de compra por `DocEntry`.
|
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
|
## Catálogos
|
||||||
|
|
||||||
### `GET /api/Currencies`
|
### `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
|
## Lotes
|
||||||
|
|
||||||
### `GET /api/BatchNumberDetails`
|
### `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
|
## Resumen de Códigos de Respuesta
|
||||||
|
|
||||||
| Método | Éxito | Cuerpo en éxito | Error SAP | Error de conexión |
|
| Método | Éxito | Cuerpo en éxito | Error SAP | Error de conexión |
|
||||||
|
|||||||
@@ -12,24 +12,32 @@ graph TB
|
|||||||
subgraph "Core"
|
subgraph "Core"
|
||||||
SAP[core/sap.py\nSAP Session & Proxy]
|
SAP[core/sap.py\nSAP Session & Proxy]
|
||||||
SES[core/session.py\nEmployee Sessions]
|
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
|
end
|
||||||
|
|
||||||
subgraph "Routers"
|
subgraph "Routers"
|
||||||
A[auth.py\nLogin / Logout]
|
A[auth.py\nLogin / Logout]
|
||||||
BP[business_partners.py\nSocios de Negocio]
|
BP[business_partners.py\nSocios de Negocio]
|
||||||
IT[items.py\nArtículos]
|
IT[items.py\nArtículos]
|
||||||
SL[sales.py\nFacturas / Pedidos]
|
SL[sales.py\nFacturas / Pedidos / Guías]
|
||||||
PR[purchases.py\nÓrdenes / Facturas Compra]
|
PR[purchases.py\nÓrdenes / Facturas / Guías Compra]
|
||||||
CT[catalogs.py\nCatálogos]
|
CT[catalogs.py\nCatálogos]
|
||||||
BT[batch.py\nLotes]
|
BT[batch.py\nLotes]
|
||||||
PM[permissions.py\nPermisos de Menú]
|
PM[permissions.py\nPermisos de Menú]
|
||||||
|
APG[apigateway.py\nCrystal Reports]
|
||||||
|
RP[reports.py\nReportes Jinja/HTML → PDF]
|
||||||
end
|
end
|
||||||
|
|
||||||
MP[menu_permissions.json]
|
MP[menu_permissions.json]
|
||||||
|
TPL[templates/reports/*.html]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "External"
|
subgraph "External"
|
||||||
SAP_SL[SAP B1 Service Layer]
|
SAP_SL[SAP B1 Service Layer]
|
||||||
|
SAP_AG[SAP B1 API Gateway\nrs/v1/*]
|
||||||
|
SAP_HANA[(SAP HANA)]
|
||||||
end
|
end
|
||||||
|
|
||||||
M --> C
|
M --> C
|
||||||
@@ -44,21 +52,42 @@ graph TB
|
|||||||
M --> CT
|
M --> CT
|
||||||
M --> BT
|
M --> BT
|
||||||
M --> PM
|
M --> PM
|
||||||
|
M --> APG
|
||||||
|
M --> RP
|
||||||
|
|
||||||
A --> SAP
|
A --> SAP
|
||||||
A --> SES
|
A --> SES
|
||||||
BP --> SAP
|
BP --> CRUD
|
||||||
IT --> SAP
|
IT --> CRUD
|
||||||
SL --> SAP
|
SL --> CRUD
|
||||||
PR --> SAP
|
PR --> CRUD
|
||||||
CT --> SAP
|
CT --> CRUD
|
||||||
BT --> SAP
|
BT --> CRUD
|
||||||
|
CRUD --> SAP
|
||||||
PM --> MP
|
PM --> MP
|
||||||
|
APG --> AG
|
||||||
|
RP --> HANA
|
||||||
|
RP --> TPL
|
||||||
|
|
||||||
SAP --> SAP_SL
|
SAP --> SAP_SL
|
||||||
SES --> SAP
|
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
|
## Pipeline de Middleware
|
||||||
|
|
||||||
Las peticiones atraviesan los siguientes middleware en orden:
|
Las peticiones atraviesan los siguientes middleware en orden:
|
||||||
@@ -70,7 +99,7 @@ flowchart LR
|
|||||||
CID --> SES_MW[session_middleware\nValida X-Session-Token]
|
CID --> SES_MW[session_middleware\nValida X-Session-Token]
|
||||||
SES_MW --> ROUTER{Router}
|
SES_MW --> ROUTER{Router}
|
||||||
ROUTER -->|/api/Login| A[auth.py]
|
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
|
### Detalle de Middleware
|
||||||
@@ -79,71 +108,86 @@ flowchart LR
|
|||||||
| -------------------------- | ----------------------------------------------------------- |
|
| -------------------------- | ----------------------------------------------------------- |
|
||||||
| `CORSMiddleware` | Permite peticiones desde cualquier origen |
|
| `CORSMiddleware` | Permite peticiones desde cualquier origen |
|
||||||
| `correlation_id_middleware` | Genera o extrae `X-Request-ID` para trazabilidad en logs |
|
| `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
|
## Arquitectura de Sesiones
|
||||||
|
|
||||||
Existen **dos capas de sesión independientes**:
|
Existen **tres capas de sesión/credenciales independientes**:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph Employee["Employee Session"]
|
subgraph Employee["Employee Session"]
|
||||||
T["Token aleatorio → emp_id"]
|
T["Token aleatorio → webUser"]
|
||||||
E["employee_sessions dict"]
|
E["employee_sessions dict"]
|
||||||
TK["active_tokens dict"]
|
TK["active_tokens dict"]
|
||||||
T --> TK
|
T --> TK
|
||||||
TK --> E
|
TK --> E
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph SAP["SAP B1 Session"]
|
subgraph SAP["SAP B1 Service Layer Session"]
|
||||||
S["sap_session_store"]
|
S["sap_session_store"]
|
||||||
M["Manager Credentials (.env)"]
|
MC["Manager Credentials (.env: SAPB1_SL_*)"]
|
||||||
S -.->|Renueva en 401| M
|
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
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
| Capa | Almacenamiento | Ámbito | Timeout | Renovación |
|
| Capa | Almacenamiento | Ámbito | Timeout | Renovación |
|
||||||
| ------------- | ----------------- | -------- | ------------- | -------------------- |
|
| -------------- | -------------------- | ------------ | ------------------- | --------------------------- |
|
||||||
| SAP B1 | `sap_session_store` | Global | Gestionado SAP | Automática al recibir 401 |
|
| SAP Service Layer | `sap_session_store` (`core/sap.py`) | Global | Gestionado SAP | Automática al recibir 401 |
|
||||||
| Empleado | `active_tokens` | Por empleado | 30 min inactividad | Login manual |
|
| 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
|
## Estructura de Rutas
|
||||||
|
|
||||||
| Método | Ruta | Router | Descripción |
|
| Método | Ruta | Router | Descripción |
|
||||||
| -------- | --------------------------------- | -------------------- | ------------------------ |
|
| -------- | --------------------------------- | --------------------- | ------------------------ |
|
||||||
| `POST` | `/api/Login` | `auth.py` | Autenticar empleado |
|
| `POST` | `/api/Login` | `auth.py` | Autenticar empleado |
|
||||||
| `POST` | `/api/Logout` | `auth.py` | Cerrar sesión |
|
| `POST` | `/api/Logout` | `auth.py` | Cerrar sesión |
|
||||||
| `GET` | `/api/BusinessPartners` | `business_partners` | Listar socios |
|
| `GET` | `/api/BusinessPartners` | `business_partners` | Listar socios |
|
||||||
| `GET` | `/api/BusinessPartners/{code}` | `business_partners` | Obtener socio |
|
| `GET` | `/api/BusinessPartners/{code}` | `business_partners` | Obtener socio |
|
||||||
| `POST` | `/api/BusinessPartners` | `business_partners` | Crear socio |
|
| `POST` | `/api/BusinessPartners` | `business_partners` | Crear socio |
|
||||||
| `PATCH` | `/api/BusinessPartners/{code}` | `business_partners` | Actualizar socio |
|
| `PATCH` | `/api/BusinessPartners/{code}` | `business_partners` | Actualizar socio |
|
||||||
| `GET` | `/api/Items` | `items.py` | Listar artículos |
|
| `GET` | `/api/Items` | `items.py` | Listar artículos |
|
||||||
| `GET` | `/api/Items/{code}` | `items.py` | Obtener artículo |
|
| `GET` | `/api/Items/{code}` | `items.py` | Obtener artículo |
|
||||||
| `POST` | `/api/Items` | `items.py` | Crear artículo |
|
| `POST` | `/api/Items` | `items.py` | Crear artículo |
|
||||||
| `PATCH` | `/api/Items/{code}` | `items.py` | Actualizar artículo |
|
| `PATCH` | `/api/Items/{code}` | `items.py` | Actualizar artículo |
|
||||||
| `GET` | `/api/Invoices` | `sales.py` | Listar facturas |
|
| `GET/POST/PATCH` | `/api/Invoices[/{key}]` | `sales.py` | Facturas de venta |
|
||||||
| `GET` | `/api/Invoices/{key}` | `sales.py` | Obtener factura |
|
| `GET/POST/PATCH` | `/api/Orders[/{key}]` | `sales.py` | Pedidos de venta |
|
||||||
| `POST` | `/api/Invoices` | `sales.py` | Crear factura |
|
| `GET/POST/PATCH` | `/api/DeliveryNotes[/{key}]` | `sales.py` | Guías de remisión de venta |
|
||||||
| `PATCH` | `/api/Invoices/{key}` | `sales.py` | Actualizar factura |
|
| `GET/PATCH` | `/api/PurchaseOrders[/{key}]` | `purchases.py` | Órdenes de compra |
|
||||||
| `GET` | `/api/Orders` | `sales.py` | Listar pedidos |
|
| `GET/PATCH` | `/api/PurchaseDeliveryNotes[/{key}]` | `purchases.py` | Guías de recepción de compra |
|
||||||
| `GET` | `/api/Orders/{key}` | `sales.py` | Obtener pedido |
|
| `GET/PATCH` | `/api/PurchaseInvoices[/{key}]` | `purchases.py` | Facturas de compra |
|
||||||
| `POST` | `/api/Orders` | `sales.py` | Crear pedido |
|
| `GET` | `/api/Currencies` | `catalogs.py` | Listar monedas |
|
||||||
| `PATCH` | `/api/Orders/{key}` | `sales.py` | Actualizar pedido |
|
| `GET` | `/api/BusinessPartnerGroups` | `catalogs.py` | Listar grupos de socios |
|
||||||
| `GET` | `/api/PurchaseOrders` | `purchases.py` | Listar órdenes de compra |
|
| `GET` | `/api/ItemGroups` | `catalogs.py` | Listar grupos artículos |
|
||||||
| `GET` | `/api/PurchaseOrders/{key}` | `purchases.py` | Obtener orden de compra |
|
| `GET` | `/api/Warehouses` | `catalogs.py` | Listar almacenes |
|
||||||
| `PATCH` | `/api/PurchaseOrders/{key}` | `purchases.py` | Actualizar orden compra |
|
| `GET` | `/api/SalesTaxCodes` | `catalogs.py` | Listar códigos de IVA |
|
||||||
| `GET` | `/api/PurchaseInvoices` | `purchases.py` | Listar facturas compra |
|
| `GET` | `/api/FactoringIndicators` | `catalogs.py` | Listar indicadores de factoring |
|
||||||
| `GET` | `/api/PurchaseInvoices/{key}` | `purchases.py` | Obtener factura compra |
|
| `GET` | `/api/Sunat/BusinessPartners/DocumentType` | `catalogs.py` | Tipos de documento SUNAT (campo de usuario `EXX_TIPODOCU`) |
|
||||||
| `PATCH` | `/api/PurchaseInvoices/{key}` | `purchases.py` | Actualizar factura compra|
|
| `GET` | `/api/BatchNumberDetails` | `batch.py` | Listar lotes |
|
||||||
| `GET` | `/api/Currencies` | `catalogs.py` | Listar monedas |
|
| `GET` | `/api/BatchNumberDetails/{id}` | `batch.py` | Obtener lote |
|
||||||
| `GET` | `/api/BusinessPartnerGroups` | `catalogs.py` | Listar grupos de socios |
|
| `PATCH` | `/api/BatchNumberDetails/{id}` | `batch.py` | Actualizar lote |
|
||||||
| `GET` | `/api/Warehouses` | `catalogs.py` | Listar almacenes |
|
| `GET` | `/api/user/menu` | `permissions.py` | Obtener menú permitido |
|
||||||
| `GET` | `/api/ItemGroups` | `catalogs.py` | Listar grupos artículos |
|
| `GET` | `/api/apigateway/reports` | `apigateway.py` | Listar reportes Crystal Reports autorizados |
|
||||||
| `GET` | `/api/SalesTaxCodes` | `catalogs.py` | Listar códigos de IVA |
|
| `GET` | `/api/apigateway/reports/{docCode}/parameters` | `apigateway.py` | Parámetros de un reporte Crystal Reports |
|
||||||
| `GET` | `/api/BatchNumberDetails` | `batch.py` | Listar lotes |
|
| `POST` | `/api/apigateway/reports/{docCode}/export` | `apigateway.py` | Exportar reporte Crystal Reports a PDF |
|
||||||
| `GET` | `/api/BatchNumberDetails/{id}` | `batch.py` | Obtener lote |
|
| `POST` | `/api/reports/stickers` | `reports.py` | Generar PDF/HTML de stickers de guía de remisión (consulta directa a HANA + plantilla Jinja + WeasyPrint) |
|
||||||
| `PATCH` | `/api/BatchNumberDetails/{id}` | `batch.py` | Actualizar lote |
|
|
||||||
| `GET` | `/api/user/menu` | `permissions.py` | Obtener menú permitido |
|
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
|
## Dependencias del Proyecto
|
||||||
|
|
||||||
@@ -160,24 +204,44 @@ graph TD
|
|||||||
main --> catalogs
|
main --> catalogs
|
||||||
main --> batch
|
main --> batch
|
||||||
main --> permissions
|
main --> permissions
|
||||||
|
main --> apigateway_router[apigateway]
|
||||||
|
main --> reports_router[reports]
|
||||||
|
|
||||||
auth --> sap_core
|
auth --> sap_core
|
||||||
auth --> session_core
|
auth --> session_core
|
||||||
business_partners --> sap_core
|
business_partners --> crud_router
|
||||||
items --> sap_core
|
items --> crud_router
|
||||||
sales --> sap_core
|
sales --> crud_router
|
||||||
purchases --> sap_core
|
purchases --> crud_router
|
||||||
|
catalogs --> crud_router
|
||||||
catalogs --> sap_core
|
catalogs --> sap_core
|
||||||
batch --> sap_core
|
batch --> crud_router
|
||||||
|
crud_router --> sap_core
|
||||||
permissions --> menu_json[menu_permissions.json]
|
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
|
## Configuración por Entorno
|
||||||
|
|
||||||
La aplicación carga siempre `.env` (un solo archivo, sin sufijos de entorno).
|
La aplicación carga siempre `mini-backend/.env` (un solo archivo por
|
||||||
`.env.aberio` se conserva como plantilla de referencia; para desplegar contra
|
despliegue, sin sufijos de entorno) mediante `load_dotenv`. Ese archivo
|
||||||
ese entorno, copiar su contenido dentro de `.env`.
|
**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 |
|
Grupos de variables:
|
||||||
| ------------------ | ------------------------------- | ----------------------- |
|
|
||||||
| `.env` | `192.168.1.175:50000` | `SBO_PRUEBA_TAGUM_22` |
|
| Grupo | Variables | Usado por |
|
||||||
| `.env.aberio` | `192.168.9.3:50000` | `PRUEBA_20260609` |
|
| --------------------- | -------------------------------------------------------------------------- | --------------------- |
|
||||||
|
| 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
|
participant SAP as SAP Service Layer
|
||||||
|
|
||||||
F->>API: POST /api/Login\n{Username, Password}
|
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})")
|
API->>SAP_C: forward_to_sapb1(GET, "EmployeesInfo")\n$filter=U_WEB_USER eq '{webUser}'
|
||||||
SAP_C->>SAP: GET /b1s/v1/EmployeesInfo({empId})
|
SAP_C->>SAP: GET /b1s/v1/EmployeesInfo?$filter=...
|
||||||
SAP-->>SAP_C: {FirstName, LastName, ...}
|
SAP-->>SAP_C: {EmployeeID, FirstName, LastName, Active, U_WEB_PASS, ...}
|
||||||
SAP_C-->>API: Employee data
|
SAP_C-->>API: Employee data (o lista vacía)
|
||||||
|
|
||||||
API->>API: ¿Password == LastName?
|
API->>API: ¿Existe empleado?\n¿Active == "tYES"?\n¿U_WEB_PASS == Password?
|
||||||
alt Coincide
|
alt Todo válido
|
||||||
API->>S: create_employee_session(emp_id, first_name, last_name)
|
API->>S: create_employee_session(webUser, empId, first_name, last_name)
|
||||||
S->>S: Genera token_urlsafe(32)
|
S->>S: Genera token_urlsafe(32)\n(invalida token anterior del mismo webUser)
|
||||||
S-->>API: token
|
S-->>API: token
|
||||||
|
|
||||||
API->>SAP_C: ensure_sap_session()
|
API->>SAP_C: ensure_sap_session()
|
||||||
@@ -29,9 +29,9 @@ sequenceDiagram
|
|||||||
SAP-->>SAP_C: B1SESSION cookie
|
SAP-->>SAP_C: B1SESSION cookie
|
||||||
SAP_C-->>API: OK
|
SAP_C-->>API: OK
|
||||||
|
|
||||||
API-->>F: 200 {message, token, username}
|
API-->>F: 200 {message, token, username: FirstName, company: CompanyDB}
|
||||||
else No coincide
|
else No existe / inactivo / password no coincide
|
||||||
API-->>F: 401 Invalid credentials
|
API-->>F: 401 (mensaje según el caso: "Usuario no registrado",\n"Usuario inactivo" o "Usuario o contraseña incorrectos")
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -68,8 +68,8 @@ sequenceDiagram
|
|||||||
|
|
||||||
alt Token válido
|
alt Token válido
|
||||||
S->>S: Actualiza last_activity
|
S->>S: Actualiza last_activity
|
||||||
S-->>MW: emp_id
|
S-->>MW: webUser
|
||||||
MW->>MW: Adjunta emp_id a request.state
|
MW->>MW: set_current_user(webUser, full_name, emp_id)\nrequest.state.webUser = webUser
|
||||||
MW->>R: Continúa al router
|
MW->>R: Continúa al router
|
||||||
R-->>F: 200 Response
|
R-->>F: 200 Response
|
||||||
else Token expirado (>30 min)
|
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
|
## Stack Tecnológico
|
||||||
|
|
||||||
@@ -17,29 +17,34 @@ Backend de integración entre una aplicación frontend (Next.js) y **SAP Busines
|
|||||||
```mermaid
|
```mermaid
|
||||||
graph TB
|
graph TB
|
||||||
subgraph "Frontend"
|
subgraph "Frontend"
|
||||||
F[App SN - Next.js]
|
F[mini-frontend - React + Vite]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "Backend SN - FastAPI"
|
subgraph "Backend - FastAPI"
|
||||||
API[API Proxy\n:8000]
|
API[API Proxy\n:8000]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "SAP Business One"
|
subgraph "SAP Business One"
|
||||||
SL[Service Layer\nREST API]
|
SL[Service Layer\nREST API]
|
||||||
DB[(SAP DB\nSBO_PRUEBA_*)]
|
AG[API Gateway\nCrystal Reports]
|
||||||
|
DB[(SAP DB / HANA)]
|
||||||
end
|
end
|
||||||
|
|
||||||
F -->|HTTP /api/*| API
|
F -->|HTTP /api/*| API
|
||||||
API -->|HTTP /b1s/v1| SL
|
API -->|HTTP /b1s/v1| SL
|
||||||
|
API -->|HTTP rs/v1/*| AG
|
||||||
|
API -->|SQL directo| DB
|
||||||
SL --> DB
|
SL --> DB
|
||||||
|
AG --> DB
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentos
|
## Documentos
|
||||||
|
|
||||||
| Documento | Descripción |
|
| Documento | Descripción |
|
||||||
| -------------------------------------------- | -------------------------------------------- |
|
| ---------------------------------------- | ---------------------------------------------- |
|
||||||
| [Arquitectura](architecture.md) | Componentes, dependencias, middlewares, APIs |
|
| [Arquitectura](architecture.md) | Componentes, dependencias, middlewares, APIs |
|
||||||
| [Flujo de Autenticación](auth-flow.md) | Login, logout, sesiones, middleware |
|
| [Flujo de Autenticación](auth-flow.md) | Login, logout, sesiones, middleware |
|
||||||
| [Proxy SAP](sap-proxy.md) | Enrutamiento a SAP, re-autenticación, OData |
|
| [Proxy SAP](sap-proxy.md) | Enrutamiento a SAP, re-autenticación, OData |
|
||||||
| [API Reference](api-reference.md) | Todos los endpoints, ejemplos request/response |
|
| [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` |
|
| [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)
|
> Nota: este documento describía originalmente un plan pendiente. Ya está
|
||||||
|
> implementado tal como se describe abajo; se conserva el nombre del
|
||||||
- `main.py:32` fuerza `setup_logging(environment="debug")` siempre — el modo
|
> archivo por los enlaces existentes desde `index.md`.
|
||||||
`"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')}")`
|
|
||||||
|
|
||||||
## Objetivo
|
## Objetivo
|
||||||
|
|
||||||
Además de arreglar lo anterior, dejar un log **por sesión de usuario
|
Cada línea de log queda etiquetada automáticamente con el `request_id` de
|
||||||
conectado**: que cada línea de log quede etiquetada con qué empleado
|
la petición HTTP y con el empleado conectado que la originó (`webUser`,
|
||||||
(`emp_id` + nombre) hizo qué acción (`GET /Items`, `POST /BusinessPartners`,
|
`emp_id`, `full_name`), en formato JSON, sin tener que pasar esos datos a
|
||||||
etc.), en JSON, para poder cargarlo después en un monitor/dashboard y buscar
|
mano en cada llamada a `logger.info/warning/error`.
|
||||||
acciones por usuario.
|
|
||||||
|
|
||||||
## Diseño
|
## Piezas
|
||||||
|
|
||||||
`context.py` ya tiene un `ContextVar` para el `request_id` que se inyecta
|
### `context.py`
|
||||||
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**.
|
|
||||||
|
|
||||||
### 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` — expuesta via `get_request_id()` / `set_request_id()`.
|
||||||
`request_id_ctx`:
|
- `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
|
### `main.py`
|
||||||
current_user_ctx: ContextVar[dict] = ContextVar(
|
|
||||||
"current_user", default={"emp_id": "-", "username": "-"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_current_user() -> dict:
|
- `correlation_id_middleware` llama a `set_request_id()` con el header
|
||||||
return current_user_ctx.get()
|
`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):
|
### `logger_config.py` — `JsonFormatter`
|
||||||
current_user_ctx.set({"emp_id": emp_id, "username": username})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. `core/session.py`
|
En modo `"production"` (default), cada línea de log —tanto en consola como
|
||||||
|
en `logs/backend.log`— es un JSON con esta forma:
|
||||||
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:
|
|
||||||
|
|
||||||
```json
|
```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(...)`,
|
En cualquier otro valor de `LOG_ENVIRONMENT` (por ejemplo `"development"`),
|
||||||
agregando `extra={"http_method": method, "endpoint": endpoint_path,
|
la consola usa un formato de texto plano legible
|
||||||
"status_code": response.status_code}` en las líneas de request/respuesta,
|
(`%(asctime)s [%(levelname)s] %(module)s.%(funcName)s:%(lineno)d - %(message)s`)
|
||||||
para que queden como campos estructurados y no solo texto libre. Quitar la
|
en vez de JSON — pensado para desarrollo local, no para buscar en el
|
||||||
variable `I = " "` (indentación para prints, ya no hace falta) y los
|
archivo. El archivo `logs/backend.log` (rotativo, 5 MB x 3 backups vía
|
||||||
emojis (el nivel del log ya indica severidad).
|
`RotatingFileHandler`) siempre se escribe en JSON sin importar el entorno.
|
||||||
|
|
||||||
### 6. `routers/auth.py`
|
## Qué NO se loguea
|
||||||
|
|
||||||
- Agregar `logger = logging.getLogger(__name__)`.
|
`routers/auth.py` nunca escribe `Password` / `U_WEB_PASS` en los logs, ni en
|
||||||
- Reemplazar todos los `print()` por `logger.info/warning/error(...)`.
|
éxito ni en fallo de login — solo el `webUser`, el `EmployeeID` y si el
|
||||||
- **No loguear nunca `empLastName` ni `employee_data.get('LastName')`** —
|
intento fue exitoso o el motivo del rechazo (`"Usuario no registrado"`,
|
||||||
solo el `emp_id` y si el login fue exitoso o no.
|
`"Usuario inactivo"`, contraseña incorrecta). Al auditar un cambio en el
|
||||||
- En cuanto se confirma el empleado (login exitoso), llamar
|
flujo de login, verificar que esta regla se mantenga.
|
||||||
`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).
|
|
||||||
|
|
||||||
### 7. `docker-compose.yml`
|
## Verificación
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
1. Levantar el backend (local o Docker).
|
1. Levantar el backend (local o Docker).
|
||||||
2. Loguearse con un usuario de prueba y hacer un par de llamadas reales
|
2. Loguearse con un usuario de prueba y hacer un par de llamadas reales
|
||||||
(GET a algún catálogo, POST/PATCH a algo).
|
(GET a algún catálogo, POST/PATCH a algo).
|
||||||
3. Revisar `logs/backend.log`: cada línea debe traer `emp_id`, `username`,
|
3. Revisar `logs/backend.log`: cada línea debe traer `webUser`, `emp_id`,
|
||||||
`http_method`, `endpoint`, `status_code` correctos.
|
`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
|
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**.
|
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
|
# Visualización de logs
|
||||||
|
|
||||||
El backend escribe en `logs/backend.log` una línea JSON por evento (ver
|
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`,
|
[logging-plan.md](logging-plan.md)), con campos como `webUser`, `emp_id`,
|
||||||
`http_method`, `endpoint` y `status_code`. Es el formato correcto para
|
`full_name`, `http_method`, `endpoint` y `status_code`. Es el formato
|
||||||
buscar/cargar en un dashboard, pero incómodo de leer a simple vista con
|
correcto para buscar/cargar en un dashboard, pero incómodo de leer a simple
|
||||||
`cat`/`tail`. Estas son las dos formas recomendadas de verlo cómodo desde la
|
vista con `cat`/`tail`. Estas son las dos formas recomendadas de verlo
|
||||||
terminal.
|
cómodo desde la terminal.
|
||||||
|
|
||||||
## `jq` — filtrar y colorear en vivo
|
## `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
|
# seguir el log en vivo, con formato e indentación
|
||||||
tail -f logs/backend.log | jq .
|
tail -f logs/backend.log | jq .
|
||||||
|
|
||||||
# solo las líneas de un empleado puntual
|
# solo las líneas de un empleado puntual (emp_id es string, ej. "11")
|
||||||
tail -f logs/backend.log | jq 'select(.emp_id == 10)'
|
tail -f logs/backend.log | jq 'select(.emp_id == "11")'
|
||||||
|
|
||||||
# solo errores/warnings
|
# solo errores/warnings
|
||||||
tail -f logs/backend.log | jq 'select(.level == "ERROR" or .level == "WARNING")'
|
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")'
|
tail -f logs/backend.log | jq 'select(.endpoint == "Items")'
|
||||||
|
|
||||||
# ver solo unos campos, en una línea por evento
|
# 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
|
## `lnav` — visor interactivo
|
||||||
@@ -41,9 +41,9 @@ lnav logs/backend.log
|
|||||||
- colores por nivel (`INFO`/`WARNING`/`ERROR`).
|
- colores por nivel (`INFO`/`WARNING`/`ERROR`).
|
||||||
- búsqueda de texto con `/`.
|
- búsqueda de texto con `/`.
|
||||||
- filtros en vivo con `:filter-in` / `:filter-out` (por ejemplo
|
- 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
|
- 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
|
Es la opción más cómoda para explorar el log completo o investigar un
|
||||||
incidente puntual sin escribir comandos `jq` largos.
|
incidente puntual sin escribir comandos `jq` largos.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Función Central: `forward_to_sapb1`
|
## 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
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -116,3 +116,15 @@ sequenceDiagram
|
|||||||
| Error de red | Lanza `ConnectionError` |
|
| Error de red | Lanza `ConnectionError` |
|
||||||
| Reintento también falla | Lanza `HTTPException` con el mensaje de error SAP |
|
| Reintento también falla | Lanza `HTTPException` con el mensaje de error SAP |
|
||||||
| SAP no disponible | `HTTPException(500)` |
|
| 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.
|
||||||
@@ -11,7 +11,7 @@ dotenv==0.9.9
|
|||||||
fastapi==0.136.3
|
fastapi==0.136.3
|
||||||
fonttools==4.63.0
|
fonttools==4.63.0
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
hdbcli==2.29.25
|
hdbcli
|
||||||
idna==3.17
|
idna==3.17
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.6
|
||||||
MarkupSafe==3.0.3
|
MarkupSafe==3.0.3
|
||||||
@@ -22,14 +22,14 @@ pydantic_core==2.46.4
|
|||||||
pydyf==0.12.1
|
pydyf==0.12.1
|
||||||
pyphen==0.18.1
|
pyphen==0.18.1
|
||||||
python-dotenv==1.2.2
|
python-dotenv==1.2.2
|
||||||
requests==2.34.2
|
requests
|
||||||
starlette==1.2.0
|
starlette==1.2.0
|
||||||
tinycss2==1.5.1
|
tinycss2==1.5.1
|
||||||
tinyhtml5==2.1.0
|
tinyhtml5==2.1.0
|
||||||
typing-inspection==0.4.2
|
typing-inspection==0.4.2
|
||||||
typing_extensions==4.15.0
|
typing_extensions==4.15.0
|
||||||
urllib3==2.7.0
|
urllib3
|
||||||
uvicorn==0.48.0
|
uvicorn
|
||||||
weasyprint==69.0
|
weasyprint==69.0
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
zopfli==0.4.3
|
zopfli==0.4.3
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Dashboard
|
||||||
|
|
||||||
|
Página de inicio del ERP (`/dashboard`). Muestra KPIs, accesos rápidos a las operaciones más comunes y un feed de actividad reciente, organizados en secciones con grillas de cards.
|
||||||
|
|
||||||
|
**Importante:** actualmente todo el contenido de `Dashboard.tsx` es **estático/hardcodeado** en el propio componente (arrays `kpiCards`, `actionCards`, `newsCards`, `activityItems`). No hay ningún fetch a `api.ts` ni al backend — los valores de ventas, compras, clientes e inventario son de ejemplo, no datos reales de SAP. La sección "Noticias y guías" está deshabilitada en código (`{false && (...)}`), por lo que no se renderiza aunque el array `newsCards` exista.
|
||||||
|
|
||||||
|
## Árbol de Componentes
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
DASH[Dashboard.tsx]
|
||||||
|
SEC[DashboardSection.tsx]
|
||||||
|
GRID[DashboardGrid.tsx]
|
||||||
|
CARD[DashboardCard.tsx]
|
||||||
|
KPI[DashboardKpiCard.tsx]
|
||||||
|
ACT[DashboardActionCard.tsx]
|
||||||
|
NEWS[DashboardNewsCard.tsx]
|
||||||
|
|
||||||
|
DASH -->|"KPI"| SEC
|
||||||
|
DASH -->|"Accesos rápidos"| SEC
|
||||||
|
DASH -->|"Noticias y guías (deshabilitada)"| SEC
|
||||||
|
DASH -->|"Actividad reciente (bloque propio, sin card)"| ACTIVITY[div de actividad inline]
|
||||||
|
|
||||||
|
SEC --> GRID
|
||||||
|
GRID --> CARD
|
||||||
|
CARD -->|type: kpi| KPI
|
||||||
|
CARD -->|type: action| ACT
|
||||||
|
CARD -->|type: news| NEWS
|
||||||
|
```
|
||||||
|
|
||||||
|
`DashboardCard` es un dispatcher: según la prop `type` (`"kpi" | "action" | "news"`) delega el render en `DashboardKpiCard`, `DashboardActionCard` o `DashboardNewsCard`. El bloque "Actividad reciente" no usa `DashboardCard`: es una lista renderizada directamente en `Dashboard.tsx`.
|
||||||
|
|
||||||
|
## Tipos de card
|
||||||
|
|
||||||
|
| Card | Props principales | Comportamiento |
|
||||||
|
|---|---|---|
|
||||||
|
| `DashboardKpiCard` | `title`, `icon` (componente de `lucide-react`), `value`, `subtitle`, `action?: {label, to}` | Muestra un valor numérico/texto grande. Si tiene `action`, renderiza un botón que navega con `useNavigate()` a la ruta `to`. |
|
||||||
|
| `DashboardActionCard` | `title`, `icon`, `to` | Card clickeable completa (ícono + título) que navega a `to` con `useNavigate()`. Tiene hover que cambia el fondo a `--color-primary-active`. |
|
||||||
|
| `DashboardNewsCard` | `image`, `title`, `description` | Card con imagen de fondo (`backgroundImage`) + texto. Actualmente usa URLs externas de `picsum.photos` como placeholder. No implementada en pantalla porque su sección está deshabilitada. |
|
||||||
|
|
||||||
|
Todas las cards usan variables CSS del tema (`--color-surface`, `--color-border`, `--color-primary`, `--color-text-muted`, etc.) vía `style` inline, no clases de Tailwind.
|
||||||
|
|
||||||
|
## Archivos Clave
|
||||||
|
|
||||||
|
| Archivo | Ruta | Propósito |
|
||||||
|
|---|---|---|
|
||||||
|
| `Dashboard.tsx` | `src/views/` | Vista principal; define los datos hardcodeados de KPIs, accesos rápidos, noticias y actividad, y arma las secciones |
|
||||||
|
| `DashboardSection.tsx` | `src/components/dashboard/` | Título de sección (`h2`) + contenedor de su contenido |
|
||||||
|
| `DashboardGrid.tsx` | `src/components/dashboard/` | Grid CSS responsivo (`auto-fill`, `minmax(260px, 1fr)`) para las cards |
|
||||||
|
| `DashboardCard.tsx` | `src/components/dashboard/` | Dispatcher que elige la card concreta según `type` |
|
||||||
|
| `DashboardKpiCard.tsx` | `src/components/dashboard/` | Card de indicador numérico con botón de acción opcional |
|
||||||
|
| `DashboardActionCard.tsx` | `src/components/dashboard/` | Card de acceso rápido, navega al hacer clic |
|
||||||
|
| `DashboardNewsCard.tsx` | `src/components/dashboard/` | Card de noticia con imagen (actualmente sin uso, sección deshabilitada) |
|
||||||
|
|
||||||
|
## Pendiente / a tener en cuenta
|
||||||
|
|
||||||
|
Si se decide conectar el dashboard a datos reales, haría falta: (1) reemplazar los arrays estáticos por llamadas a `api.ts` hacia endpoints del backend que hoy no existen para este propósito (no hay un endpoint de KPIs agregados en `mini-backend`), y (2) decidir si se reactiva la sección de noticias y de dónde saldría el contenido (hoy usa imágenes externas de `picsum.photos`, no aptas para producción).
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Flujo de Procesamiento — Datos Maestros de Artículos
|
||||||
|
|
||||||
|
## Arquitectura General
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Frontend React (sap-frontend)"
|
||||||
|
RT[Router App.tsx]
|
||||||
|
LIST[ItemsList.tsx]
|
||||||
|
LIST_TMPL[ListViewTemplate.tsx]
|
||||||
|
VISTA[Items.tsx]
|
||||||
|
FORM[ItemForm.tsx]
|
||||||
|
UI_FF[FormattedField.tsx]
|
||||||
|
UI_SEL[SelectField.tsx]
|
||||||
|
UI_TABLE[DenseTable.tsx]
|
||||||
|
API[api.ts]
|
||||||
|
ENUMS[enums.ts]
|
||||||
|
TYPES[types/sapItems.ts]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Backend FastAPI (mini-backend)"
|
||||||
|
ROUTER[routers/items.py<br/>make_crud_router]
|
||||||
|
CORE[core/crud_router.py]
|
||||||
|
SAPCORE[core/sap.py<br/>forward_to_sapb1]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "SAP Business One"
|
||||||
|
SL[Service Layer<br/>Items / ItemGroups]
|
||||||
|
end
|
||||||
|
|
||||||
|
RT --> LIST
|
||||||
|
RT --> VISTA
|
||||||
|
LIST --> LIST_TMPL
|
||||||
|
LIST_TMPL --> UI_TABLE
|
||||||
|
LIST_TMPL --> API
|
||||||
|
VISTA --> FORM
|
||||||
|
FORM --> UI_FF
|
||||||
|
FORM --> UI_SEL
|
||||||
|
FORM --> API
|
||||||
|
FORM --> ENUMS
|
||||||
|
FORM --> TYPES
|
||||||
|
|
||||||
|
API -->|GET/POST/PATCH| ROUTER
|
||||||
|
ROUTER --> CORE
|
||||||
|
CORE --> SAPCORE
|
||||||
|
SAPCORE -->|B1SESSION| SL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diferencia clave respecto al flujo de Lotes:** `ItemForm.tsx` no hace mapeo de nombres SAP→amigable (no existe un `mapSapToFriendly`). El estado del formulario (`SapItem`, en `types/sapItems.ts`) usa directamente los nombres de campo de SAP (`ItemCode`, `ItemName`, `ItemsGroupCode`, etc.), tanto para mostrar como para enviar al backend. Tampoco hay tabla de mapeo de nombres en esta sección por ese motivo.
|
||||||
|
|
||||||
|
En el backend, `routers/items.py` no tiene lógica propia: es una instancia de la factory genérica `make_crud_router("Items", key_field="ItemCode", key_quoted=True)` definida en `core/crud_router.py` (ver `mini-backend/docs/architecture.md`), que arma las cuatro rutas CRUD y reenvía a SAP vía `core/sap.py`.
|
||||||
|
|
||||||
|
## Flujo Vista de Lista → Editar/Crear Artículo
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User as Usuario
|
||||||
|
participant Router as App.tsx
|
||||||
|
participant ListView as ItemsList
|
||||||
|
participant LVT as ListViewTemplate
|
||||||
|
participant DTable as DenseTable
|
||||||
|
participant DetailView as Items
|
||||||
|
participant Form as ItemForm
|
||||||
|
participant API as api.ts
|
||||||
|
participant Backend as backend items.py
|
||||||
|
participant SAP as SAP Service Layer
|
||||||
|
|
||||||
|
User->>Router: /inventario/articulos
|
||||||
|
Router->>ListView: render
|
||||||
|
ListView->>API: GET /api/ItemGroups (para mapear ItemsGroupCode → nombre en la columna "Grupo")
|
||||||
|
ListView->>LVT: <ListViewTemplate endpoint="Items" baseFilter="AssetItem eq 'tNO'" .../>
|
||||||
|
LVT->>API: GET /api/Items?skip=0&top=50&fields=ItemCode,ItemName,...
|
||||||
|
API->>Backend: fetch con X-Session-Token
|
||||||
|
Backend->>SAP: GET Items (con B1SESSION)
|
||||||
|
SAP-->>Backend: JSON con datos
|
||||||
|
Backend-->>API: JSON response
|
||||||
|
API-->>LVT: json.value[]
|
||||||
|
LVT->>DTable: render rows
|
||||||
|
DTable-->>User: tabla con artículos (filtros por Grupo y Clase de artículo)
|
||||||
|
|
||||||
|
User->>DTable: clic en editar (ItemCode "A0001")
|
||||||
|
DTable->>Router: navigate("/inventario/articulos/mantenimiento?code=A0001")
|
||||||
|
Router->>DetailView: render con searchParams.code
|
||||||
|
DetailView->>Form: <ItemForm itemCode="A0001" backPath="/inventario/articulos" />
|
||||||
|
Form->>API: GET /api/ItemGroups (para poblar el SelectField de grupo)
|
||||||
|
Form->>API: GET /api/Items/A0001
|
||||||
|
API->>Backend: fetch
|
||||||
|
Backend->>SAP: GET Items('A0001')
|
||||||
|
SAP-->>Backend: {ItemCode, ItemName, ItemsGroupCode, ItemType, ...}
|
||||||
|
Backend-->>API: JSON raw SAP
|
||||||
|
API-->>Form: data
|
||||||
|
Form->>Form: setItem(data) (sin mapeo de nombres)
|
||||||
|
Form-->>User: formulario con valores cargados
|
||||||
|
|
||||||
|
User->>Form: edita campos y clic "Actualizar Artículo"
|
||||||
|
Form->>API: PATCH /api/Items/A0001
|
||||||
|
Note over Form,API: body: {ItemName, BarCode, ItemsGroupCode, ItemType, ...}
|
||||||
|
API->>Backend: fetch PATCH
|
||||||
|
Backend->>SAP: PATCH Items('A0001')
|
||||||
|
SAP-->>Backend: 204 No Content
|
||||||
|
Backend-->>API: 204
|
||||||
|
API-->>Form: ok
|
||||||
|
Form-->>User: "Artículo actualizado exitosamente"
|
||||||
|
|
||||||
|
Note over User,SAP: Si itemCode no viene en la URL (alta), el botón dice<br/>"Crear Artículo" y el submit hace POST /api/Items en vez de PATCH.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Árbol de Componentes
|
||||||
|
|
||||||
|
```
|
||||||
|
App.tsx (BrowserRouter)
|
||||||
|
└── / (ProtectedRoute → MainLayout)
|
||||||
|
└── /inventario
|
||||||
|
├── /articulos
|
||||||
|
│ └── ItemsList.tsx
|
||||||
|
│ └── ListViewTemplate.tsx
|
||||||
|
│ ├── SearchInput.tsx
|
||||||
|
│ ├── PrimaryButton.tsx
|
||||||
|
│ ├── DenseTable.tsx
|
||||||
|
│ │ └── formatValue() [formatters.ts]
|
||||||
|
│ └── PaginationBar.tsx
|
||||||
|
└── /articulos/mantenimiento
|
||||||
|
└── Items.tsx
|
||||||
|
└── ItemForm.tsx
|
||||||
|
├── FormattedField.tsx (x6)
|
||||||
|
└── SelectField.tsx (x3: ItemsGroupCode, ItemType, CostAccountingMethod)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Campos del Formulario (nombres SAP, sin mapeo)
|
||||||
|
|
||||||
|
| Campo SAP | Tipo | Componente | Notas |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ItemCode` | input text | FormattedField | obligatorio, deshabilitado implícitamente en edición (se usa como key de URL) |
|
||||||
|
| `ItemName` | input text | FormattedField | obligatorio |
|
||||||
|
| `BarCode` | input text | FormattedField | |
|
||||||
|
| `ItemsGroupCode` | select | SelectField | opciones cargadas desde `GET /api/ItemGroups` |
|
||||||
|
| `ItemType` | select | SelectField | opciones estáticas `ITEM_TYPE` (`utils/enums.ts`) |
|
||||||
|
| `CostAccountingMethod` | select | SelectField | opciones estáticas `GL_METHOD` (`utils/enums.ts`) |
|
||||||
|
| `SalesUnit` | input text | FormattedField | |
|
||||||
|
| `PurchaseUnit` | input text | FormattedField | |
|
||||||
|
| `InventoryUOM` | input text | FormattedField | |
|
||||||
|
| `AvgStdPrice` | currency, solo lectura | FormattedField (`mode="display"`) | |
|
||||||
|
| `PurchaseItem`, `InventoryItem`, `ManageSerialNumbers`, `ManageBatchNumbers`, `Valid`, `Frozen` | checkbox | input nativo | mapean a `"tYES"`/`"tNO"` vía `toCheck`/`fromCheck` |
|
||||||
|
|
||||||
|
## Archivos Clave
|
||||||
|
|
||||||
|
| Archivo | Ruta | Propósito |
|
||||||
|
|---|---|---|
|
||||||
|
| `Items.tsx` | `src/views/inventario/` | Vista detalle, extrae `code` de la URL y renderiza `ItemForm` (sirve tanto para alta como edición) |
|
||||||
|
| `ItemForm.tsx` | `src/components/` | Formulario alta/edición, fetch + POST/PATCH sin mapeo de nombres |
|
||||||
|
| `ItemsList.tsx` | `src/views/inventario/` | Lista con filtros por grupo de artículos y clase de artículo, columnas con formato de moneda |
|
||||||
|
| `SelectField.tsx` | `src/components/ui/` | Select reutilizable |
|
||||||
|
| `FormattedField.tsx` | `src/components/ui/` | Campo display/input con formato |
|
||||||
|
| `ListViewTemplate.tsx` | `src/components/ui/` | Template genérico de listado con paginación/búsqueda/filtros |
|
||||||
|
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
|
||||||
|
| `enums.ts` | `src/utils/` | Opciones estáticas `ITEM_TYPE`, `GL_METHOD` |
|
||||||
|
| `sapItems.ts` | `src/types/` | Interfaz TypeScript `SapItem` |
|
||||||
|
| `items.py` | `mini-backend/routers/` | Instancia de `make_crud_router` para `Items` (GET lista, GET por código, POST, PATCH) |
|
||||||
|
| `crud_router.py` | `mini-backend/core/` | Factory genérica de routers CRUD reutilizada por `items.py` y otros routers |
|
||||||
|
| `sap.py` | `mini-backend/core/` | Proxy universal a SAP Service Layer |
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Flujo de Procesamiento — Documentos de Marketing (Ventas / Compras)
|
||||||
|
|
||||||
|
Cubre los documentos comerciales de SAP Business One que la app gestiona: Órdenes de Venta, Facturas de Venta, Órdenes de Compra y Facturas de Proveedor. Los cuatro comparten un único formulario genérico (`MarketingDocumentForm.tsx`) parametrizado por vista.
|
||||||
|
|
||||||
|
## Arquitectura General
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Frontend React (sap-frontend)"
|
||||||
|
OL[OrdersList.tsx]
|
||||||
|
O[Orders.tsx]
|
||||||
|
IL[InvoicesList.tsx]
|
||||||
|
I[Invoices.tsx]
|
||||||
|
POL[PurchaseOrdersList.tsx]
|
||||||
|
PO[PurchaseOrders.tsx]
|
||||||
|
PIL[PurchaseInvoicesList.tsx]
|
||||||
|
PI[PurchaseInvoices.tsx]
|
||||||
|
FORM[MarketingDocumentForm.tsx]
|
||||||
|
COPY[CopyFromModal.tsx]
|
||||||
|
SEARCH[ModalSearchTemplate.tsx]
|
||||||
|
BADGE[StatusBadge.tsx]
|
||||||
|
LVT[ListViewTemplate.tsx]
|
||||||
|
TYPES[types/sapMarketing.ts]
|
||||||
|
API[api.ts]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Backend FastAPI (mini-backend)"
|
||||||
|
SALES[routers/sales.py]
|
||||||
|
PURCH[routers/purchases.py]
|
||||||
|
CORE[core/sap.py]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "SAP Business One"
|
||||||
|
SL[Service Layer<br/>Orders / Invoices / PurchaseOrders / PurchaseInvoices]
|
||||||
|
end
|
||||||
|
|
||||||
|
OL --> LVT
|
||||||
|
IL --> LVT
|
||||||
|
POL --> LVT
|
||||||
|
PIL --> LVT
|
||||||
|
LVT --> BADGE
|
||||||
|
|
||||||
|
O --> FORM
|
||||||
|
I --> FORM
|
||||||
|
PO --> FORM
|
||||||
|
PI --> FORM
|
||||||
|
FORM --> COPY
|
||||||
|
FORM --> SEARCH
|
||||||
|
FORM --> TYPES
|
||||||
|
FORM --> API
|
||||||
|
|
||||||
|
API -->|GET/POST/PATCH| SALES
|
||||||
|
API -->|GET/POST/PATCH| PURCH
|
||||||
|
SALES --> CORE
|
||||||
|
PURCH --> CORE
|
||||||
|
CORE --> SL
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada vista de mantenimiento (`Orders.tsx`, `Invoices.tsx`, `PurchaseOrders.tsx`, `PurchaseInvoices.tsx`) es una capa delgada: mantiene el estado del documento (`doc`, tipado como `MarketingDocument` en `types/sapMarketing.ts`) y le pasa a `MarketingDocumentForm` la configuración específica de ese tipo de documento — el componente de formulario en sí no sabe si está editando una orden o una factura, solo renderiza lo que recibe por props.
|
||||||
|
|
||||||
|
## Cómo un solo formulario sirve a los 4 documentos
|
||||||
|
|
||||||
|
`MarketingDocumentForm` recibe por props todo lo que varía entre documentos:
|
||||||
|
|
||||||
|
| Prop | Qué controla | Ejemplo |
|
||||||
|
|---|---|---|
|
||||||
|
| `endpoint` | Endpoint del backend a consultar/enviar (`GET/POST/PATCH /api/{endpoint}`) | `"Orders"`, `"Invoices"`, `"PurchaseOrders"`, `"PurchaseInvoices"` |
|
||||||
|
| `tipoDocumento` | Etiqueta tipada (`OrdenVenta`, `FacturaVenta`, `OrdenCompra`, `FacturaProveedor`) declarada en `DocumentConfigProps` | — no se usa dentro de la lógica del propio formulario en el código leído; parece ser solo metadata para quien lo invoca |
|
||||||
|
| `titulo` | Encabezado visible del formulario | `"Factura de Ventas (OINV)"` |
|
||||||
|
| `headerExtraFields` | Campos de cabecera exclusivos de ese documento | Facturas: `FolioPrefixString`, `FolioNumber`, `GroupNum` |
|
||||||
|
| `lineColumns` | Columnas de la grilla de líneas, cada una con un `show(doc)` opcional para ocultar/mostrar según `DocType` | Compras oculta `ItemCode`/`Almacén` si `DocType` es de servicio |
|
||||||
|
| `copyFromSources` | Documentos origen habilitados para el botón "📋 Copiar de" | Facturas de Venta: Orden de Venta y Entrega; Facturas de Proveedor: Orden de Compra |
|
||||||
|
| `retencionConfig` / `detraccionConfig` | Solo presentes en `PurchaseInvoices.tsx` — habilitan los bloques de Retención (3%/6%) y Detracción (SUNAT) en el resumen de totales | No se pasan en Órdenes ni en Facturas de Venta |
|
||||||
|
| `onBuscarSocio`, `onBuscarArticulos`, `onCellSearch` | Callbacks que abren los `ModalSearchTemplate` de Socios, Artículos, Almacén y Código de Impuesto | Cada vista instancia sus propios modales y decide qué endpoint/filtro usar |
|
||||||
|
|
||||||
|
El cálculo de totales (subtotal, impuesto 18% fijo, retención, detracción, total general) vive dentro del propio `MarketingDocumentForm`, no en las vistas.
|
||||||
|
|
||||||
|
## Flujo: Crear una Factura de Venta copiando una Orden
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User as Usuario
|
||||||
|
participant List as InvoicesList
|
||||||
|
participant View as Invoices.tsx
|
||||||
|
participant Form as MarketingDocumentForm
|
||||||
|
participant Copy as CopyFromModal
|
||||||
|
participant API as api.ts
|
||||||
|
participant Backend as backend sales.py
|
||||||
|
participant SAP as SAP Service Layer
|
||||||
|
|
||||||
|
User->>List: /ventas/facturas → "Nueva Factura"
|
||||||
|
List->>View: navigate("/ventas/facturas/mantenimiento")
|
||||||
|
View->>Form: <MarketingDocumentForm endpoint="Invoices" copyFromSources=[Orden, Entrega] />
|
||||||
|
Form->>API: GET /api/Currencies
|
||||||
|
API->>Backend: fetch
|
||||||
|
Backend->>SAP: GET Currencies
|
||||||
|
SAP-->>Form: lista de monedas
|
||||||
|
|
||||||
|
User->>Form: busca y selecciona Cliente (🔍)
|
||||||
|
Form->>View: onBuscarSocio() abre ModalSearchTemplate(BusinessPartners)
|
||||||
|
View-->>Form: setDoc({CardCode, CardName})
|
||||||
|
|
||||||
|
User->>Form: clic "📋 Copiar de" → "Orden de Venta"
|
||||||
|
Form->>Copy: <CopyFromModal source={Orden} cardCode docType />
|
||||||
|
Copy->>API: GET /api/Orders?filtro por CardCode/BaseType
|
||||||
|
API->>Backend: fetch
|
||||||
|
Backend->>SAP: GET Orders
|
||||||
|
SAP-->>Copy: documentos abiertos del cliente
|
||||||
|
User->>Copy: selecciona una orden
|
||||||
|
Copy-->>Form: onConfirm(lines, sourceLabel)
|
||||||
|
Note over Form: Si ya hay líneas, confirm() pregunta reemplazar o agregar
|
||||||
|
Form->>Form: setDoc({DocumentLines: lines}) — cada línea trae BaseType/BaseEntry/BaseLine
|
||||||
|
|
||||||
|
User->>Form: clic "Crear Documento"
|
||||||
|
Form->>API: POST /api/Invoices (doc + DocumentLines, sin BaseRef)
|
||||||
|
API->>Backend: fetch POST
|
||||||
|
Backend->>SAP: POST Invoices (con líneas BaseEntry → SAP cierra la orden origen)
|
||||||
|
SAP-->>Backend: 201 Created
|
||||||
|
Backend-->>Form: ok
|
||||||
|
Form-->>User: "Documento creado exitosamente en SAP"
|
||||||
|
```
|
||||||
|
|
||||||
|
El mismo patrón aplica a Facturas de Proveedor copiando desde Órdenes de Compra, solo cambian `endpoint` y `copyFromSources`.
|
||||||
|
|
||||||
|
## Árbol de Componentes
|
||||||
|
|
||||||
|
```
|
||||||
|
App.tsx (BrowserRouter)
|
||||||
|
└── / (ProtectedRoute → MainLayout)
|
||||||
|
├── /ventas
|
||||||
|
│ ├── /ordenes → OrdersList.tsx → ListViewTemplate → StatusBadge
|
||||||
|
│ ├── /ordenes/mantenimiento → Orders.tsx → MarketingDocumentForm
|
||||||
|
│ ├── /entrega → stub "Entregas / Guías" (sin vista real, ver Hallazgos)
|
||||||
|
│ └── /facturas
|
||||||
|
│ ├── (lista) → InvoicesList.tsx → ListViewTemplate → StatusBadge
|
||||||
|
│ └── /mantenimiento → Invoices.tsx → MarketingDocumentForm
|
||||||
|
└── /compras
|
||||||
|
├── /ordenes → PurchaseOrdersList.tsx → ListViewTemplate → StatusBadge
|
||||||
|
├── /ordenes/mantenimiento → PurchaseOrders.tsx → MarketingDocumentForm
|
||||||
|
├── /recepcion → stub "Recepciones de Compra" (sin vista real, ver Hallazgos)
|
||||||
|
└── /facturas
|
||||||
|
├── (lista) → PurchaseInvoicesList.tsx → ListViewTemplate → StatusBadge
|
||||||
|
└── /mantenimiento → PurchaseInvoices.tsx → MarketingDocumentForm (+ Retención/Detracción)
|
||||||
|
|
||||||
|
MarketingDocumentForm.tsx
|
||||||
|
├── FormattedField.tsx / SelectField.tsx (cabecera)
|
||||||
|
├── CopyFromModal.tsx (botón "Copiar de")
|
||||||
|
└── (por vista) ModalSearchTemplate.tsx ×4 (Socios, Artículos, Almacén, Código de Impuesto)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Archivos Clave
|
||||||
|
|
||||||
|
| Archivo | Ruta | Propósito |
|
||||||
|
|---|---|---|
|
||||||
|
| `MarketingDocumentForm.tsx` | `src/components/` | Formulario genérico: cabecera, grilla de líneas, totales, copiar-de, submit GET/POST/PATCH |
|
||||||
|
| `Orders.tsx` / `Invoices.tsx` | `src/views/ventas/` | Configuran el formulario para Órdenes y Facturas de venta |
|
||||||
|
| `PurchaseOrders.tsx` / `PurchaseInvoices.tsx` | `src/views/compras/` | Configuran el formulario para Compras; Facturas agrega retención/detracción |
|
||||||
|
| `OrdersList.tsx`, `InvoicesList.tsx`, `PurchaseOrdersList.tsx`, `PurchaseInvoicesList.tsx` | `src/views/ventas/`, `src/views/compras/` | Listados vía `ListViewTemplate`, con `StatusBadge` por estado del documento |
|
||||||
|
| `CopyFromModal.tsx` | `src/components/ui/` | Modal para copiar líneas desde un documento base (Orden→Entrega→Factura) |
|
||||||
|
| `ModalSearchTemplate.tsx` | `src/components/ui/` | Modal de búsqueda genérico reutilizado para Socios, Artículos, Almacenes, Códigos de Impuesto |
|
||||||
|
| `sapMarketing.ts` | `src/types/` | `MarketingDocument`, `DocumentLine`, `DocumentConfigProps`, `CopyFromSource`, `LineColumnConfig` |
|
||||||
|
| `sunat.ts` | `src/utils/` | `DETRACCION_CODIGOS` (porcentajes SUNAT usados en Facturas de Proveedor) |
|
||||||
|
| `sales.py` / `purchases.py` | `mini-backend/routers/` | Endpoints REST que proxean a SAP Service Layer |
|
||||||
|
|
||||||
|
## Hallazgos (código tal cual está hoy, sin corregir)
|
||||||
|
|
||||||
|
- **`ventas/entrega` y `compras/recepcion` son stubs**: en `App.tsx` son componentes en línea que solo muestran un título ("Entregas / Guías" / "Recepciones de Compra"), sin listado ni formulario real todavía.
|
||||||
|
- **`DeliveryNotesList.tsx` existe pero no está enrutado**: el archivo `src/views/ventas/DeliveryNotesList.tsx` está escrito pero ninguna ruta en `App.tsx` lo importa o lo usa — no es alcanzable desde la navegación actual.
|
||||||
|
- El endpoint `PurchaseDeliveryNotes` (documentado en `mini-backend/docs/api-reference.md`) tampoco tiene vista de frontend correspondiente todavía.
|
||||||
@@ -18,7 +18,7 @@ graph TB
|
|||||||
TYPES[types/sapBatch.ts]
|
TYPES[types/sapBatch.ts]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "Backend FastAPI (backend-sn)"
|
subgraph "Backend FastAPI (mini-backend)"
|
||||||
ROUTER[routers/batch.py]
|
ROUTER[routers/batch.py]
|
||||||
CORE[core/sap.py<br/>forward_to_sapb1]
|
CORE[core/sap.py<br/>forward_to_sapb1]
|
||||||
end
|
end
|
||||||
@@ -156,5 +156,5 @@ App.tsx (BrowserRouter)
|
|||||||
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
|
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
|
||||||
| `enums.ts` | `src/utils/` | Mapeos enum SAP (BATCH_STATUS_APROB, etc.) |
|
| `enums.ts` | `src/utils/` | Mapeos enum SAP (BATCH_STATUS_APROB, etc.) |
|
||||||
| `sapBatch.ts` | `src/types/` | Interfaz TypeScript BatchNumberDetail |
|
| `sapBatch.ts` | `src/types/` | Interfaz TypeScript BatchNumberDetail |
|
||||||
| `batch.py` | `backend-sn/routers/` | Rutas FastAPI GET/PATCH BatchNumberDetails |
|
| `batch.py` | `mini-backend/routers/` | Rutas FastAPI GET/PATCH BatchNumberDetails |
|
||||||
| `sap.py` | `backend-sn/core/` | Proxy universal a SAP Service Layer |
|
| `sap.py` | `mini-backend/core/` | Proxy universal a SAP Service Layer |
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Flujo de Procesamiento — Socios de Negocio (Clientes / Proveedores)
|
||||||
|
|
||||||
|
## Arquitectura General
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Frontend React (sap-frontend)"
|
||||||
|
RT[Router App.tsx]
|
||||||
|
CLIST[CustomersList.tsx]
|
||||||
|
SLIST[SuppliersList.tsx]
|
||||||
|
CVIEW[Customers.tsx]
|
||||||
|
SVIEW[Suppliers.tsx]
|
||||||
|
BPFORM[BusinessPartnerForm.tsx]
|
||||||
|
LIST_TMPL[ListViewTemplate.tsx]
|
||||||
|
UI_TABLE[DenseTable.tsx]
|
||||||
|
API[api.ts]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Backend FastAPI (mini-backend)"
|
||||||
|
ROUTER[routers/business_partners.py<br/>make_crud_router]
|
||||||
|
CORE[core/crud_router.py]
|
||||||
|
SAP_CORE[core/sap.py]
|
||||||
|
SUNAT[Sunat/BusinessPartners/DocumentType]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "SAP Business One"
|
||||||
|
SL[Service Layer<br/>BusinessPartners]
|
||||||
|
end
|
||||||
|
|
||||||
|
RT --> CLIST
|
||||||
|
RT --> SLIST
|
||||||
|
RT --> CVIEW
|
||||||
|
RT --> SVIEW
|
||||||
|
CLIST --> LIST_TMPL
|
||||||
|
SLIST --> LIST_TMPL
|
||||||
|
CVIEW --> BPFORM
|
||||||
|
SVIEW --> BPFORM
|
||||||
|
LIST_TMPL --> UI_TABLE
|
||||||
|
LIST_TMPL --> API
|
||||||
|
BPFORM --> API
|
||||||
|
|
||||||
|
API -->|GET/POST/PATCH BusinessPartners| ROUTER
|
||||||
|
API -->|GET DocumentType| SUNAT
|
||||||
|
ROUTER --> CORE
|
||||||
|
CORE --> SAP_CORE
|
||||||
|
SAP_CORE -->|B1SESSION| SL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Un solo formulario para Clientes y Proveedores
|
||||||
|
|
||||||
|
A diferencia de otros módulos, acá **no hay un componente por tipo de socio**: `BusinessPartnerForm.tsx` es un único formulario genérico parametrizado por props, y `Customers.tsx` / `Suppliers.tsx` son wrappers finos que solo le pasan configuración distinta:
|
||||||
|
|
||||||
|
| Prop | `Customers.tsx` | `Suppliers.tsx` |
|
||||||
|
| ----------------- | --------------------------- | --------------------------------------------------- |
|
||||||
|
| `cardType` | `"cCustomer"` | `"cSupplier"` |
|
||||||
|
| `entityLabel` | `"Cliente"` | `"Proveedor"` |
|
||||||
|
| `listPath` | `/socios/clientes` | `/socios/proveedores` |
|
||||||
|
| `codePlaceholder` | `Ej. C00001` | `Ej. P00001` |
|
||||||
|
| `extraFields` | `[]` (sin campos extra hoy) | `U_EXX_TIPODOCU` (Tipo de Documento SUNAT, select) |
|
||||||
|
|
||||||
|
La diferenciación entre "es cliente" o "es proveedor" para el propio dato de SAP la hace el campo `CardType` (`cCustomer` / `cSupplier`), que viaja fijo en el `body` del POST/PATCH según qué wrapper se use — el formulario en sí no lo deja editar.
|
||||||
|
|
||||||
|
El mecanismo `extraFields` (tipo `ExtraFieldConfig[]`, definido en `BusinessPartnerForm.tsx`) permite agregar campos exclusivos de un tipo de socio sin tocar el formulario base: cada entrada define `name`, `label`, `type` (`text` | `email` | `select`), y opcionalmente `loadOptions` (una función async que trae las opciones del backend). Hoy el único caso real es el campo `U_EXX_TIPODOCU` en Proveedores, que carga sus opciones desde `GET /api/Sunat/BusinessPartners/DocumentType` (catálogo SUNAT expuesto vía SAP).
|
||||||
|
|
||||||
|
## Flujo Vista de Lista → Editar Socio (caso Clientes)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User as Usuario
|
||||||
|
participant Router as App.tsx
|
||||||
|
participant ListView as CustomersList
|
||||||
|
participant LVT as ListViewTemplate
|
||||||
|
participant DTable as DenseTable
|
||||||
|
participant DetailView as Customers.tsx
|
||||||
|
participant Form as BusinessPartnerForm
|
||||||
|
participant API as api.ts
|
||||||
|
participant Backend as backend business_partners.py
|
||||||
|
participant SAP as SAP Service Layer
|
||||||
|
|
||||||
|
User->>Router: /socios/clientes
|
||||||
|
Router->>ListView: render
|
||||||
|
ListView->>LVT: <ListViewTemplate endpoint="BusinessPartners" baseFilter="CardType eq 'cCustomer'" .../>
|
||||||
|
LVT->>API: GET /api/BusinessPartners?...&$filter=CardType eq 'cCustomer'
|
||||||
|
API->>Backend: fetch con X-Session-Token
|
||||||
|
Backend->>SAP: GET BusinessPartners (con B1SESSION)
|
||||||
|
SAP-->>Backend: JSON con datos
|
||||||
|
Backend-->>API: JSON response
|
||||||
|
API-->>LVT: json.value[]
|
||||||
|
LVT->>DTable: render rows
|
||||||
|
DTable-->>User: tabla de clientes
|
||||||
|
|
||||||
|
User->>DTable: clic en editar (CardCode "C00001")
|
||||||
|
DTable->>Router: navigate("/socios/clientes/mantenimiento?code=C00001")
|
||||||
|
Router->>DetailView: render con searchParams.code
|
||||||
|
DetailView->>Form: <BusinessPartnerForm cardType="cCustomer" .../>
|
||||||
|
Form->>API: GET /api/BusinessPartners/C00001
|
||||||
|
API->>Backend: fetch
|
||||||
|
Backend->>SAP: GET BusinessPartners('C00001')
|
||||||
|
SAP-->>Backend: {CardCode, CardName, FederalTaxID, BPAddresses[], ...}
|
||||||
|
Backend-->>API: JSON raw SAP
|
||||||
|
API-->>Form: data
|
||||||
|
Form->>Form: setFormData(data) — sin mapeo de nombres, usa los campos SAP tal cual
|
||||||
|
Form-->>User: formulario con pestañas "General" / "Direcciones"
|
||||||
|
|
||||||
|
User->>Form: edita campos y clic "Actualizar Cliente"
|
||||||
|
Form->>API: PATCH /api/BusinessPartners/C00001
|
||||||
|
Note over Form,API: body: {CardName, FederalTaxID, Phone1, EmailAddress, BPAddresses[], ...extraFields}
|
||||||
|
API->>Backend: fetch PATCH
|
||||||
|
Backend->>SAP: PATCH BusinessPartners('C00001')
|
||||||
|
SAP-->>Backend: 204 No Content
|
||||||
|
Backend-->>API: 204
|
||||||
|
API-->>Form: ok
|
||||||
|
Form-->>User: "Cliente actualizado correctamente"
|
||||||
|
```
|
||||||
|
|
||||||
|
El flujo de Proveedores es idéntico, cambiando el `baseFilter` a `CardType eq 'cSupplier'` en `SuppliersList.tsx` y usando `Suppliers.tsx` como vista de detalle.
|
||||||
|
|
||||||
|
Para un socio **nuevo** (`isEditMode = false`), el submit hace `POST /api/BusinessPartners` en vez de `PATCH`, y tras la respuesta exitosa navega de vuelta al listado (`listPath`) después de 1.5s.
|
||||||
|
|
||||||
|
## Árbol de Componentes
|
||||||
|
|
||||||
|
```
|
||||||
|
App.tsx (BrowserRouter)
|
||||||
|
└── / (ProtectedRoute → MainLayout)
|
||||||
|
└── /socios
|
||||||
|
├── /clientes
|
||||||
|
│ └── CustomersList.tsx
|
||||||
|
│ └── ListViewTemplate.tsx
|
||||||
|
│ ├── SearchInput.tsx
|
||||||
|
│ ├── PrimaryButton.tsx
|
||||||
|
│ ├── DenseTable.tsx
|
||||||
|
│ └── PaginationBar.tsx
|
||||||
|
├── /clientes/mantenimiento
|
||||||
|
│ └── Customers.tsx
|
||||||
|
│ └── BusinessPartnerForm.tsx (cardType="cCustomer")
|
||||||
|
├── /proveedores
|
||||||
|
│ └── SuppliersList.tsx
|
||||||
|
│ └── ListViewTemplate.tsx (mismo árbol que CustomersList)
|
||||||
|
└── /proveedores/mantenimiento
|
||||||
|
└── Suppliers.tsx
|
||||||
|
└── BusinessPartnerForm.tsx (cardType="cSupplier", extraFields=[U_EXX_TIPODOCU])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Campos del Formulario
|
||||||
|
|
||||||
|
A diferencia del módulo de Lotes, acá **no hay mapeo de nombres SAP → amigable**: el formulario usa los nombres de campo de SAP directamente como `name` de cada input (`CardCode`, `CardName`, `FederalTaxID`, `Phone1`, `EmailAddress`).
|
||||||
|
|
||||||
|
| Campo SAP | Pestaña | Tipo | Notas |
|
||||||
|
| ------------------------- | ------------ | ------------------------------ | ------------------------------------------------------------- |
|
||||||
|
| `CardCode` | General | input text (solo lectura en edición) | Llave primaria en SAP; no editable una vez creado el socio |
|
||||||
|
| `CardName` | General | input text | Razón social / nombre |
|
||||||
|
| `CardType` | General | fijo, no editable en UI | `cCustomer` o `cSupplier` según el wrapper usado |
|
||||||
|
| `FederalTaxID` | General | input text | RUC / documento de identidad |
|
||||||
|
| `Phone1` | General | input text | Teléfono principal |
|
||||||
|
| `EmailAddress` | General | input email | Correo |
|
||||||
|
| `U_EXX_TIPODOCU` | General | select (dinámico, solo Proveedores) | Catálogo SUNAT vía `/api/Sunat/BusinessPartners/DocumentType` |
|
||||||
|
| `BPAddresses[].Street`, `City`, `County`, `GlobalLocationNumber` | Direcciones | input text | Dos direcciones por defecto: `bo_BillTo` (Facturación) y `bo_ShipTo` (Despacho) |
|
||||||
|
|
||||||
|
## Archivos Clave
|
||||||
|
|
||||||
|
| Archivo | Ruta | Propósito |
|
||||||
|
| ------------------------- | --------------------------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `BusinessPartnerForm.tsx` | `src/components/` | Formulario genérico único para Clientes y Proveedores, parametrizable |
|
||||||
|
| `Customers.tsx` | `src/views/socios/` | Wrapper que configura `BusinessPartnerForm` para `cCustomer` |
|
||||||
|
| `Suppliers.tsx` | `src/views/socios/` | Wrapper que configura `BusinessPartnerForm` para `cSupplier` + campo SUNAT |
|
||||||
|
| `CustomersList.tsx` | `src/views/socios/` | Lista de clientes, filtra `CardType eq 'cCustomer'` |
|
||||||
|
| `SuppliersList.tsx` | `src/views/socios/` | Lista de proveedores, filtra `CardType eq 'cSupplier'` |
|
||||||
|
| `ListViewTemplate.tsx` | `src/components/ui/` | Template genérico de listado con paginación/búsqueda/filtros |
|
||||||
|
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
|
||||||
|
| `business_partners.py` | `mini-backend/routers/` | Router CRUD genérico (`make_crud_router("BusinessPartners", key_field="CardCode")`) |
|
||||||
|
| `crud_router.py` | `mini-backend/core/` | Factory que genera GET/POST/PATCH proxificados hacia SAP |
|
||||||
|
| `sap.py` | `mini-backend/core/` | Proxy universal a SAP Service Layer |
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Frontend mini-erp — Documentación del Proyecto
|
||||||
|
|
||||||
|
SPA en React 19 + TypeScript + Vite + Tailwind CSS + React Router. Consume la API del backend (`mini-backend`) para operar sobre SAP Business One: socios de negocio, artículos, documentos de venta/compra, lotes y reportes.
|
||||||
|
|
||||||
|
## Documentos
|
||||||
|
|
||||||
|
| Documento | Descripción |
|
||||||
|
| ----------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| [Dashboard](dashboard.md) | Pantalla de inicio: KPIs, accesos rápidos y noticias (hoy estático) |
|
||||||
|
| [Socios de Negocio](flujo-socios-negocio.md) | Clientes y proveedores, formulario compartido |
|
||||||
|
| [Artículos](flujo-articulos.md) | Alta y edición de artículos de inventario |
|
||||||
|
| [Lotes](flujo-lotes.md) | Aprobación/liberación de lotes (BatchNumberDetails) |
|
||||||
|
| [Documentos de Marketing](flujo-documentos-marketing.md) | Órdenes/facturas de venta y compra sobre un formulario común |
|
||||||
|
| [Reportes](reportes.md) | Los tres sistemas de reportes: HTML, Crystal e Informes |
|
||||||
|
|
||||||
|
## Convenciones
|
||||||
|
|
||||||
|
- Todos los documentos siguen el mismo formato: diagrama de arquitectura (mermaid), diagrama de secuencia del flujo principal, árbol de componentes y tabla de "Archivos Clave".
|
||||||
|
- Ver también `mini-erp/CLAUDE.md` (raíz del repo) para la arquitectura general del monorepo (`mini-backend` + `mini-frontend`) y el flujo de trabajo por cliente.
|
||||||
|
- Ver `mini-backend/docs/` para la documentación del backend (API, autenticación, proxy SAP, logging).
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Módulo de Reportes
|
||||||
|
|
||||||
|
El frontend expone **tres sistemas de reportes distintos**, con configuración, rutas y componentes propios cada uno. No son variantes del mismo flujo: apuntan a integraciones distintas del lado del backend, y uno de ellos (`informes`) no tiene backend real detrás todavía.
|
||||||
|
|
||||||
|
| Sistema | Ruta base | Config | Componente runner | Backend real |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Reportes HTML** | `/reportes-html/:module` | `config/htmlReports.ts` → `htmlReportsConfig` | `HtmlReportRunner.tsx` | `mini-backend/routers/reports.py` (Jinja2 + WeasyPrint, datos de HANA) — **sí implementado** |
|
||||||
|
| **Reportes Crystal** | `/reportes-crystal/:module` | `config/crystalReports.ts` → `crystalReportsConfig` | `CrystalReportRunner.tsx` | `mini-backend/routers/apigateway.py` (SAP B1 API Gateway / Crystal Reports Server) — **sí implementado** |
|
||||||
|
| **Informes** | `/informes/:module` | `config/reports.ts` → `reportsConfig` | `ReportRunner.tsx` | Ninguno confirmado — ver nota abajo |
|
||||||
|
|
||||||
|
### Nota sobre `informes` (`ReportRunner.tsx` + `reportsConfig`)
|
||||||
|
|
||||||
|
`reportsConfig` (en `config/reports.ts`) define reportes como "Estado de Cuenta", "Saldos de Clientes", "Ventas por Artículo", "Kardex de Artículos", etc., cada uno con un `endpoint` del tipo `/api/reports/socios/estado-cuenta`, `/api/reports/inventario/kardex`, etc. El único endpoint que `mini-backend/routers/reports.py` expone bajo `/api/reports/` es `POST /api/reports/stickers`. Ninguno de los endpoints que `reportsConfig` referencia existe en el backend actual. Esto sugiere que `ReportRunner.tsx` y `reportsConfig` son un scaffold frontend para reportes planeados a futuro, no un flujo funcional hoy — cualquier reporte del módulo "Informes" fallará al generar hasta que se implemente su endpoint correspondiente en el backend.
|
||||||
|
|
||||||
|
## Arquitectura General
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Listados (elegir reporte)"
|
||||||
|
RHL[HtmlReportsList.tsx]
|
||||||
|
RCL[CrystalReportsList.tsx]
|
||||||
|
MRL[ModuleReportsList.tsx]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Runners (parámetros + ejecución)"
|
||||||
|
HRR[HtmlReportRunner.tsx]
|
||||||
|
CRR[CrystalReportRunner.tsx]
|
||||||
|
RR[ReportRunner.tsx]
|
||||||
|
RPF[ReportParameterForm.tsx]
|
||||||
|
RRV[ReportResultsView.tsx]
|
||||||
|
MST[ModalSearchTemplate.tsx<br/>búsqueda F4]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Config"
|
||||||
|
CFG_H[htmlReportsConfig]
|
||||||
|
CFG_C[crystalReportsConfig]
|
||||||
|
CFG_R[reportsConfig]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Backend FastAPI (mini-backend)"
|
||||||
|
REP[routers/reports.py<br/>POST /api/reports/stickers]
|
||||||
|
APIGW[routers/apigateway.py<br/>GET/POST /api/apigateway/reports/*]
|
||||||
|
HANA[core/hana.py<br/>hana_connection]
|
||||||
|
AGW_CORE[core/api_gateway.py<br/>forward_to_api_gateway]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Externo"
|
||||||
|
HANADB[(HANA DB)]
|
||||||
|
SAPGW[SAP B1 API Gateway<br/>rs/v1 Crystal Reports Server]
|
||||||
|
end
|
||||||
|
|
||||||
|
RHL --> CFG_H --> HRR
|
||||||
|
RCL --> CFG_C --> CRR
|
||||||
|
MRL --> CFG_R --> RR
|
||||||
|
|
||||||
|
HRR --> RPF
|
||||||
|
CRR --> RPF
|
||||||
|
RR --> RPF
|
||||||
|
RR --> RRV
|
||||||
|
HRR -.f4-modal.-> MST
|
||||||
|
CRR -.f4-modal.-> MST
|
||||||
|
|
||||||
|
HRR -->|POST /api/reports/:endpoint| REP
|
||||||
|
REP --> HANA --> HANADB
|
||||||
|
|
||||||
|
CRR -->|POST /api/apigateway/reports/:docCode/export| APIGW
|
||||||
|
RCL -.al listar.-> APIGW
|
||||||
|
APIGW --> AGW_CORE --> SAPGW
|
||||||
|
|
||||||
|
RR -.->|"endpoint del reportsConfig<br/>(no existe en el backend)"| REP
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo real: Reporte HTML (ejemplo "stickers")
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User as Usuario
|
||||||
|
participant List as HtmlReportsList
|
||||||
|
participant Runner as HtmlReportRunner
|
||||||
|
participant Modal as ModalSearchTemplate
|
||||||
|
participant API as api.ts
|
||||||
|
participant Backend as reports.py
|
||||||
|
participant HANA as HANA
|
||||||
|
|
||||||
|
User->>List: /reportes-html/inventario
|
||||||
|
List->>List: lee htmlReportsConfig["inventario"]
|
||||||
|
List-->>User: tarjeta "Impresión de Etiquetas"
|
||||||
|
|
||||||
|
User->>Runner: clic → /reportes-html/inventario/stickers
|
||||||
|
Runner->>Runner: busca definition por docCode "stickers"
|
||||||
|
Runner-->>User: formulario (Documento f4-modal, Cantidad, Formato, Papel)
|
||||||
|
|
||||||
|
User->>Runner: clic en campo "Documento"
|
||||||
|
Runner->>Modal: abre ModalSearchTemplate (endpoint DeliveryNotes)
|
||||||
|
Modal-->>User: tabla de guías de remisión
|
||||||
|
User->>Modal: selecciona guía
|
||||||
|
Modal-->>Runner: DocEntry seleccionado
|
||||||
|
|
||||||
|
User->>Runner: completa cantidad/formato y clic "Generar Reporte"
|
||||||
|
Runner->>API: POST /api/reports/stickers {doc_entry, cantidad, formato, papel}
|
||||||
|
API->>Backend: fetch con X-Session-Token
|
||||||
|
Backend->>HANA: STICKERS_QUERY (datos de la guía por DocEntry)
|
||||||
|
HANA-->>Backend: fila con cliente, dirección, etc.
|
||||||
|
Backend->>Backend: renderiza templates/reports/stickers.html (Jinja2)
|
||||||
|
alt formato = pdf
|
||||||
|
Backend->>Backend: WeasyPrint HTML → PDF
|
||||||
|
Backend-->>API: StreamingResponse application/pdf
|
||||||
|
else formato = html
|
||||||
|
Backend-->>API: HTMLResponse
|
||||||
|
end
|
||||||
|
API-->>Runner: blob
|
||||||
|
Runner-->>User: <iframe> con vista previa
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo real: Reporte Crystal (ejemplo "DLN20015")
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor User as Usuario
|
||||||
|
participant List as CrystalReportsList
|
||||||
|
participant Runner as CrystalReportRunner
|
||||||
|
participant API as api.ts
|
||||||
|
participant Backend as apigateway.py
|
||||||
|
participant Core as core/api_gateway.py
|
||||||
|
participant SAPGW as SAP API Gateway
|
||||||
|
|
||||||
|
User->>List: /reportes-crystal/inventario
|
||||||
|
List->>List: lee crystalReportsConfig["inventario"]
|
||||||
|
List-->>User: tarjeta "Impresión de Etiquetas" (docCode DLN20015)
|
||||||
|
|
||||||
|
User->>Runner: clic → /reportes-crystal/inventario/DLN20015
|
||||||
|
Runner-->>User: formulario (Documento f4-modal, Cantidad)
|
||||||
|
|
||||||
|
User->>Runner: completa parámetros y clic "Exportar PDF"
|
||||||
|
Runner->>API: POST /api/apigateway/reports/DLN20015/export<br/>[{name, type, value}]
|
||||||
|
API->>Backend: fetch con X-Session-Token
|
||||||
|
Backend->>Core: forward_to_api_gateway(POST rs/v1/ExportPDFData?DocCode=DLN20015)
|
||||||
|
Core->>SAPGW: request con credenciales APIGATEWAY_* del .env
|
||||||
|
SAPGW-->>Core: PDF en base64
|
||||||
|
Core-->>Backend: response
|
||||||
|
Backend->>Backend: decodifica base64 → bytes
|
||||||
|
Backend-->>API: StreamingResponse application/pdf
|
||||||
|
API-->>Runner: blob
|
||||||
|
Runner-->>User: <iframe> con vista previa PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
Nota: `crystalReportsConfig` solo tiene un reporte cargado, en `inventario` (`DLN20015`, "Impresión de Etiquetas"). Los otros módulos (`socios`, `ventas`, `compras`) están declarados con arrays vacíos — no hay reportes Crystal configurados para ellos todavía. Curiosamente, el comentario en `mini-backend/routers/reports.py` indica que la consulta SQL de `stickers` (el reporte HTML) es "la query original de Crystal Reports para DLN20015, adaptada" — es decir, el mismo reporte de negocio (etiquetas de guía de remisión) existe hoy por **dos caminos en paralelo**: el Crystal Report original vía API Gateway, y su reimplementación propia vía HANA + Jinja + WeasyPrint.
|
||||||
|
|
||||||
|
## Árbol de Componentes y Rutas
|
||||||
|
|
||||||
|
```
|
||||||
|
App.tsx (BrowserRouter)
|
||||||
|
└── / (ProtectedRoute → MainLayout)
|
||||||
|
├── /informes/:module → ModuleReportsList.tsx
|
||||||
|
│ └── /informes/:module/:reportId → ReportRunner.tsx
|
||||||
|
│ ├── ReportParameterForm.tsx
|
||||||
|
│ └── ReportResultsView.tsx
|
||||||
|
├── /reportes-crystal/:module → CrystalReportsList.tsx
|
||||||
|
│ └── /reportes-crystal/:module/:docCode → CrystalReportRunner.tsx
|
||||||
|
│ └── ModalSearchTemplate.tsx (f4-modal)
|
||||||
|
└── /reportes-html/:module → HtmlReportsList.tsx
|
||||||
|
└── /reportes-html/:module/:docCode → HtmlReportRunner.tsx
|
||||||
|
└── ModalSearchTemplate.tsx (f4-modal)
|
||||||
|
```
|
||||||
|
|
||||||
|
`:module` es uno de `socios | ventas | compras | inventario` (`moduleLabels`/`moduleIcons` en `config/reports.ts`), compartido por los tres sistemas.
|
||||||
|
|
||||||
|
## Archivos Clave
|
||||||
|
|
||||||
|
| Archivo | Ruta | Propósito |
|
||||||
|
|---|---|---|
|
||||||
|
| `ModuleReportsList.tsx` | `src/views/informes/` | Lista reportes de `reportsConfig` por módulo |
|
||||||
|
| `ReportRunner.tsx` | `src/components/report/` | Ejecuta reportes de `reportsConfig` (endpoints aún no implementados en backend) |
|
||||||
|
| `ReportParameterForm.tsx` | `src/components/report/` | Formulario de parámetros para `ReportRunner` |
|
||||||
|
| `ReportResultsView.tsx` | `src/components/report/` | Renderiza tabla/totales/PDF según `outputType` |
|
||||||
|
| `CrystalReportsList.tsx` | `src/views/reportes/` | Lista reportes de `crystalReportsConfig` por módulo |
|
||||||
|
| `CrystalReportRunner.tsx` | `src/components/report/` | Arma el body y llama a `/api/apigateway/reports/:docCode/export` |
|
||||||
|
| `HtmlReportsList.tsx` | `src/views/reportes/` | Lista reportes de `htmlReportsConfig` por módulo |
|
||||||
|
| `HtmlReportRunner.tsx` | `src/components/report/` | Arma el body y llama a `/api/reports/:endpoint` |
|
||||||
|
| `ModalSearchTemplate.tsx` | `src/components/ui/` | Modal de búsqueda F4 reutilizado por ambos runners reales |
|
||||||
|
| `config/reports.ts` | `src/config/` | `reportsConfig`, `moduleLabels`, `moduleIcons` |
|
||||||
|
| `config/crystalReports.ts` | `src/config/` | `crystalReportsConfig` |
|
||||||
|
| `config/htmlReports.ts` | `src/config/` | `htmlReportsConfig` |
|
||||||
|
| `types/reports.ts` | `src/types/` | Tipos `ReportDefinition`, `F4Config`, etc. |
|
||||||
|
| `reports.py` | `mini-backend/routers/` | `POST /api/reports/stickers` (Jinja2 + WeasyPrint, datos vía `core/hana.py`) |
|
||||||
|
| `apigateway.py` | `mini-backend/routers/` | `GET /api/apigateway/reports`, `GET .../{docCode}/parameters`, `POST .../{docCode}/export` |
|
||||||
|
| `api_gateway.py` | `mini-backend/core/` | `forward_to_api_gateway`, proxy hacia el SAP API Gateway |
|
||||||
Reference in new issue
Block a user