primer commit

This commit is contained in:
George committed 2026-08-17 11:58:38 -05:00
1 parent ab3c613165
commit b2fb4e2d0a
18 files changed
+1291 -277

No files matched your search

-15
View File
@@ -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
+38
View File
@@ -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.
+32
View File
@@ -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
+208 -4
View File
@@ -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 |
+129 -65
View File
@@ -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.
+14 -14
View File
@@ -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
View File
@@ -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` |
+58 -149
View File
@@ -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.
+10 -10
View File
@@ -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.
+13 -1
View File
@@ -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.
+4 -4
View File
@@ -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
+57
View File
@@ -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).
+160
View File
@@ -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.
+3 -3
View File
@@ -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 |
+172
View File
@@ -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 |
+20
View File
@@ -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).
+187
View File
@@ -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 |