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