From b2fb4e2d0a2aab42ad740de03a484583a9cf63ec Mon Sep 17 00:00:00 2001 From: George Date: Mon, 17 Aug 2026 11:58:38 -0500 Subject: [PATCH] primer commit --- .gitignore | 15 -- CLAUDE.md | 38 ++++ mini-backend/.env.example | 32 +++ mini-backend/docs/api-reference.md | 212 +++++++++++++++++- mini-backend/docs/architecture.md | 194 ++++++++++------ mini-backend/docs/auth-flow.md | 28 +-- mini-backend/docs/index.md | 29 ++- mini-backend/docs/logging-plan.md | 207 +++++------------ mini-backend/docs/logs-viewing.md | 20 +- mini-backend/docs/sap-proxy.md | 14 +- mini-backend/requirements.txt | 8 +- mini-frontend/docs/dashboard.md | 57 +++++ mini-frontend/docs/flujo-articulos.md | 160 +++++++++++++ .../docs/flujo-documentos-marketing.md | 169 ++++++++++++++ mini-frontend/docs/flujo-lotes.md | 6 +- mini-frontend/docs/flujo-socios-negocio.md | 172 ++++++++++++++ mini-frontend/docs/index.md | 20 ++ mini-frontend/docs/reportes.md | 187 +++++++++++++++ 18 files changed, 1291 insertions(+), 277 deletions(-) create mode 100644 CLAUDE.md create mode 100644 mini-backend/.env.example create mode 100644 mini-frontend/docs/dashboard.md create mode 100644 mini-frontend/docs/flujo-articulos.md create mode 100644 mini-frontend/docs/flujo-documentos-marketing.md create mode 100644 mini-frontend/docs/flujo-socios-negocio.md create mode 100644 mini-frontend/docs/index.md create mode 100644 mini-frontend/docs/reportes.md diff --git a/.gitignore b/.gitignore index 988d151..548d0c4 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..52abcee --- /dev/null +++ b/CLAUDE.md @@ -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/`). +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. diff --git a/mini-backend/.env.example b/mini-backend/.env.example new file mode 100644 index 0000000..7389d01 --- /dev/null +++ b/mini-backend/.env.example @@ -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://:50000/b1s/v1 +SAPB1_SL_USER=manager +SAPB1_SL_PASSWORD= +SAPB1_SL_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://:60000 +APIGATEWAY_USER=manager +APIGATEWAY_PASSWORD= +APIGATEWAY_COMPANYDB= +APIGATEWAY_DBINSTANCE=NDB@:30013 +APIGATEWAY_TIMEOUT=30.0 + +# --- HANA (acceso directo a la base, ej. para reportes) --- +HANA_HOST= +HANA_PORT=30015 +HANA_USER=SYSTEM +HANA_PASSWORD= +HANA_SCHEMA= + +# --- Logging --- +# production | development (ver logger_config.py) +LOG_ENVIRONMENT=production diff --git a/mini-backend/docs/api-reference.md b/mini-backend/docs/api-reference.md index ed0b5f7..6eff352 100644 --- a/mini-backend/docs/api-reference.md +++ b/mini-backend/docs/api-reference.md @@ -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 | diff --git a/mini-backend/docs/architecture.md b/mini-backend/docs/architecture.md index c85dcbc..607f4ea 100644 --- a/mini-backend/docs/architecture.md +++ b/mini-backend/docs/architecture.md @@ -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. diff --git a/mini-backend/docs/auth-flow.md b/mini-backend/docs/auth-flow.md index e51880b..a3e062a 100644 --- a/mini-backend/docs/auth-flow.md +++ b/mini-backend/docs/auth-flow.md @@ -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) diff --git a/mini-backend/docs/index.md b/mini-backend/docs/index.md index 19d2e57..a00068e 100644 --- a/mini-backend/docs/index.md +++ b/mini-backend/docs/index.md @@ -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` | diff --git a/mini-backend/docs/logging-plan.md b/mini-backend/docs/logging-plan.md index f11dad6..a752f66 100644 --- a/mini-backend/docs/logging-plan.md +++ b/mini-backend/docs/logging-plan.md @@ -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. diff --git a/mini-backend/docs/logs-viewing.md b/mini-backend/docs/logs-viewing.md index f1fe90d..8b5ce7d 100644 --- a/mini-backend/docs/logs-viewing.md +++ b/mini-backend/docs/logs-viewing.md @@ -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. diff --git a/mini-backend/docs/sap-proxy.md b/mini-backend/docs/sap-proxy.md index fd79dcd..0a99ca4 100644 --- a/mini-backend/docs/sap-proxy.md +++ b/mini-backend/docs/sap-proxy.md @@ -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. diff --git a/mini-backend/requirements.txt b/mini-backend/requirements.txt index a442cec..76c8fbe 100644 --- a/mini-backend/requirements.txt +++ b/mini-backend/requirements.txt @@ -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 diff --git a/mini-frontend/docs/dashboard.md b/mini-frontend/docs/dashboard.md new file mode 100644 index 0000000..42ce879 --- /dev/null +++ b/mini-frontend/docs/dashboard.md @@ -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). diff --git a/mini-frontend/docs/flujo-articulos.md b/mini-frontend/docs/flujo-articulos.md new file mode 100644 index 0000000..a2da49c --- /dev/null +++ b/mini-frontend/docs/flujo-articulos.md @@ -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
make_crud_router] + CORE[core/crud_router.py] + SAPCORE[core/sap.py
forward_to_sapb1] + end + + subgraph "SAP Business One" + SL[Service Layer
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: + 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: + 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
"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 | diff --git a/mini-frontend/docs/flujo-documentos-marketing.md b/mini-frontend/docs/flujo-documentos-marketing.md new file mode 100644 index 0000000..8400358 --- /dev/null +++ b/mini-frontend/docs/flujo-documentos-marketing.md @@ -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
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: + 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: + 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. diff --git a/mini-frontend/docs/flujo-lotes.md b/mini-frontend/docs/flujo-lotes.md index 91d63ea..b172532 100644 --- a/mini-frontend/docs/flujo-lotes.md +++ b/mini-frontend/docs/flujo-lotes.md @@ -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
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 | diff --git a/mini-frontend/docs/flujo-socios-negocio.md b/mini-frontend/docs/flujo-socios-negocio.md new file mode 100644 index 0000000..088fb6b --- /dev/null +++ b/mini-frontend/docs/flujo-socios-negocio.md @@ -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
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
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: + 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: + 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 | diff --git a/mini-frontend/docs/index.md b/mini-frontend/docs/index.md new file mode 100644 index 0000000..77ab8c8 --- /dev/null +++ b/mini-frontend/docs/index.md @@ -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). diff --git a/mini-frontend/docs/reportes.md b/mini-frontend/docs/reportes.md new file mode 100644 index 0000000..96c3a64 --- /dev/null +++ b/mini-frontend/docs/reportes.md @@ -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
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
POST /api/reports/stickers] + APIGW[routers/apigateway.py
GET/POST /api/apigateway/reports/*] + HANA[core/hana.py
hana_connection] + AGW_CORE[core/api_gateway.py
forward_to_api_gateway] + end + + subgraph "Externo" + HANADB[(HANA DB)] + SAPGW[SAP B1 API Gateway
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
(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: