2026-10-13 08:09:07 -05:00
# CLAUDE.md
Aplicación web **Devolución a Proveedores** para SAP Business One. Reemplaza al addon
.NET Framework (`../SolucionLibros/` ), que **se abandona** : todo pasa a Angular + NestJS.
`../SolucionLibros/` queda solo como referencia de la lógica original — **no modificarlo** .
## Qué hace el proceso
Genera **Devoluciones de mercancías** (ORPD, ObjType 21) enlazadas a sus **Entradas de
mercancías por compra** (OPDN, ObjType 20), a partir de un listado de artículos y
cantidades pegado desde Excel. Cada ejecución se guarda en el UDO `LIB_ODEV`
(cabecera `@LIB_ODEV` , líneas `@LIB_DEV1` ).
Pasos de la UI (decididos):
1. **Pegar** : ItemCode + cantidad desde Excel (con el botón *Pegar desde el portapapeles* o
`Ctrl/Cmd+V` en el cuadro). Se valida el stock (`OITW.OnHand` ) del almacén elegido. No guarda.
2. **Procesar** : reemplaza a "Selección" del addon. Asignación FIFO contra líneas abiertas
de Entradas de mercancías **del almacén elegido** , ordenadas por `DocDate` . Genera filas
Padre (`F` , artículo) y Hijo (`S` , una por Entrada consumida). El usuario marca o desmarca
qué líneas Hijo devolver.
3. **Crear devoluciones** : agrupa las líneas marcadas sin `TargetEntry` por proveedor y
las parte en lotes de `U_PrintingLines` líneas (11 por defecto). Crea una Devolución por
lote y graba `U_TargetType` /`U_TargetEntry` en las líneas.
4. **Descargar PDF** : por cada Devolución, se pide el PDF de la Guía de despacho (DTE tipo 52)
al web service SOAP de BES, usando las credenciales de `@DTE_CONFC` . Opción de unir todos
los PDF en uno.
## Decisiones tomadas (no volver a discutir)
- Solo se crean **Devoluciones** (ObjType 21), no Solicitudes de devolución. Solo se cargan
las series de Devoluciones.
- `U_Status` de cabecera **eliminado** . El estado usa el `Status` estándar del UDO (`O` /`C` ).
- Se usan `U_TipoDespacho` y `U_IndicadorTraslado` en `@LIB_ODEV` . Se envían a la Devolución
como `U_BFE_TipoDespacho` / `U_BFE_IndTraslado` .
- Cantidades y referencias son **enteros** (`db_Numeric` ): `Quantity` , `OnHand` , `OpenQty` ,
`AllocatedQty` , `BaseEntry` , `BaseLine` , `BaseRef` , `TargetEntry` y `U_Series` .
- Producción en **HANA** ; ambiente de pruebas en **MSSQL** . Toda consulta SQL debe tener las
dos variantes, según `DB_TYPE` .
- Sin control de concurrencia: **solo puede haber una planificación abierta** . Para crear
una nueva, todas las anteriores tienen que estar cerradas.
- Después de Procesar, el usuario elige qué líneas devolver. **Una línea con `TargetEntry`
ya no se puede desmarcar** (ver `puedeCambiarSeleccion` en `libs/shared` ).
- La planificación se **cierra a mano** (`POST /planificaciones/:id/cerrar` ).
- **Reprocesar** (botón *Rehacer FIFO* del Detalle) solo vale para una planificación **abierta y sin
Devoluciones**. Las filas de un UDO no se pueden quitar por Service Layer, así que se crea una nueva
(heredando la cabecera) y, **después** de crearla, se cierra la anterior (si el cierre falla se avisa
para cerrarla a mano).
- **Crear devoluciones**: resultado **parcial por documento** . Cada Devolución se crea aparte; las
buenas graban `U_TargetType` /`U_TargetEntry` , las fallidas `U_ErrCode` /`U_ErrMsg` y se reintentan.
- **Consultas SQL** por `SQLQueries` del Service Layer (no hay conexión directa a la BD). La API
crea/actualiza `DP_Stock` y `DP_EntradasAbiertas` sola (variante según `DB_TYPE` ). El analizador
SQL del Service Layer **no admite `FORMAT`** y SAP reescribe el `SqlText` : al cambiar el SQL hay
que subir `version` en `sql-queries.definitions.ts` . Los parámetros van entre comillas simples.
2026-10-13 14:30:06 -05:00
`DP_EntradasAbiertas` (v3) lee la cantidad abierta de `PDN1.OpenInvQty` (no `OpenQty` ); el campo
de usuario `U_OpenQty` del UDO conserva su nombre.
2026-10-13 08:09:07 -05:00
- **Listado** de planificaciones **paginado en SAP** (`$top/$skip/$inlinecount` , OData v3 del SL v1),
más nueva primero, con filtros por estado, almacén y rango de fechas. Por defecto 20 por página,
máximo 100. En la pantalla: botones **Atrás** / **Siguiente** ; filtros y página quedan en la URL.
- **PDF**: no se usa el paquete `soap` ; `bes-soap.ts` arma el sobre y lee la respuesta a mano y `BesClient`
lo envía con axios (SOAPAction `http://tempuri.org/Consulta_TXTDTE` ). Credenciales y URL salen de
`U_DTE_CONFC` y **nunca** se registran ni se devuelven al navegador. Solo se consulta con el
`FolioNumber` de la Devolución: **si todavía no tiene folio no se llama a BES** y esa Devolución informa
"todavía no tiene folio" (el addon usaba el `DocNum` como respaldo; se descartó porque podría traer el DTE
de otro documento). Unir con `pdf-lib` ; un fallo en una Devolución no impide las demás.
- Transporte en la Devolución (ORPD): `RutTransporte→U_BFE_RutTrasporte` (sic), `Patente→U_BFE_Patente` ,
`RutChofer→U_BFE_RutChofer` , `NombreChofer→U_BFE_NombreChofer` . **ORPD no tiene UDF para fechas
ni hora de salida/llegada**: solo se guardan en `@LIB_ODEV` .
- **Despliegue en Windows**: un solo servicio. La API sirve también la web compilada (mismo origen: sin
CORS y con la cookie `SameSite=strict` ), con **HTTPS directo en Node** (`HTTPS_PFX_PATH` o
`HTTPS_KEY_PATH` +`HTTPS_CERT_PATH` ) y redirección opcional de HTTP (`HTTP_REDIRECT_PORT` ). El servicio
2026-10-13 08:55:29 -05:00
lo administra **NSSM** (decisión del usuario; se evaluó WinSW y no se lo consideró lo bastante estable);
nginx no es necesario. NSSM 2.24 estable puede no arrancar en Windows recientes: usar la 2.24-101. `npm run publicar` arma `release/devolucion-proveedores/` (API + web + solo dependencias de
2026-10-13 14:30:06 -05:00
producción, ~55 MB, **sin enlaces simbólicos** : `@dp/shared` va como copia real, porque la API lo
importa al ejecutarse y un enlace no sobrevive a copiar de Mac a Windows). Guía completa: `deploy/windows/DEPLOY.md` .
2026-10-13 08:09:07 -05:00
- **Logs**: la aplicación **no escribe logs a archivo** ; usa el `Logger` de Nest por consola. En
2026-10-13 08:55:29 -05:00
producción NSSM captura la salida y la rota a los 10 MB (no limita cuántos archivos conserva: hay que
limpiar los viejos). Con `NO_COLOR=1` el log sale sin códigos de
2026-10-13 08:09:07 -05:00
color. Solo se registran errores 4xx/5xx, fallos o lentitud del Service Layer y eventos de negocio.
- Aplicación web **independiente** . Login con el usuario de SAP B1 vía **Login del Service Layer** .
- Metadatos (UDT/UDF/UDO) definidos en **JSON** con formato del Service Layer. La web tiene la
pantalla **Configuración → Metadatos** con el botón **Reconstruir** , y NestJS los crea.
Los cambios que borran datos (campo con tipo distinto o campo sobrante) solo se hacen con
autorización explícita.
- **Después de cada login** se verifica que existan las tablas, los campos y el UDO. Si
falta algo, se redirige a Configuración → Metadatos. El login tiene una casilla ** "Verificar
los metadatos de SAP al ingresar"** (activa por defecto; se recuerda en `localStorage` ).
Desactivada, el guard omite la verificación; Configuración → Metadatos siempre verifica.
## Estructura
```
devolucion-proveedores-web/ npm workspaces
├── libs/shared/ @dp/shared tipos + lógica de dominio pura (ESM, se compila a dist/)
│ └── src/
│ ├── models/ auth, metadata, planificacion, catalogos (catálogos, páginas, PDF)
│ └── domain/ portapapeles.ts (parseo Excel), fifo.ts (FIFO, consolidación)
├── apps/api/ NestJS 12 ESM ("type": "module"), imports con extensión .js
│ ├── .env(.example)
│ └── src/
│ ├── config/ configuration.ts (lee .env)
│ ├── service-layer/ ServiceLayerClient (axios, cola por sesión), SessionStore (memoria)
│ ├── auth/ login/logout/me/info, SessionGuard global + @Public()
│ ├── sql-queries/ SqlQueriesService + definiciones (DP_Stock, DP_EntradasAbiertas)
│ ├── catalogos/ almacenes, series, valores UDF
│ ├── planificaciones/ service, controller, dto, udo.mapper (puro, con tests)
│ ├── pdf/ bes-soap (puro), BesClient, PdfDteService (descarga y unión)
│ ├── metadata/ analyzer (puro), service, controller
│ │ └── definitions/ user-tables.json, user-fields.json, user-objects.json,
│ │ external-dependencies.json ← fuente de verdad
│ ├── health/
│ ├── http-exception.logger.ts filtro global: deja en el log todo error 4xx/5xx
│ ├── web-estatico.ts sirve el build de Angular, fallback a index.html, cabeceras de seguridad
│ └── https.ts lectura del certificado y redirección HTTP → HTTPS
├── scripts/publicar.mjs arma la carpeta de despliegue (release/, ignorada por git)
2026-10-13 08:55:29 -05:00
├── deploy/windows/ NSSM: instalar/desinstalar-servicio.ps1, DEPLOY.md
2026-10-13 08:09:07 -05:00
└── apps/web/ Angular 22 standalone, signals, zoneless, CSS (no SCSS)
└── src/app/
├── core/ auth.service, metadata.service, planificaciones.service,
│ guards, auth.interceptor, http-error
├── layout/ shell.ts (barra y navegación), stepper.ts (4 pasos)
└── pages/
├── login/ con la casilla de verificar metadatos
├── metadatos/ Configuración → Metadatos
└── planificaciones/ listado (filtros + paginación) · nueva/ (pasos 1-2) · detalle/ (pasos 3-4)
```
### API (prefijo `/api`)
| Método | Ruta | Descripción |
|---|---|---|
| GET | `/health` | público |
| GET | `/auth/info` | público: CompanyDB y DB_TYPE configurados |
| POST | `/auth/login` | `{userName, password}` : Login en el Service Layer y cookie httpOnly `dp_sid` |
| POST | `/auth/logout` | Logout en el Service Layer |
| GET | `/auth/me` | usuario de la sesión |
| GET | `/metadata/status` | compara las definiciones contra la base SAP |
| POST | `/metadata/rebuild` | `{permitirDestructivos: boolean}` : crea y corrige |
| GET | `/catalogos` | almacenes, series de Devoluciones (con la por defecto), valores de `BFE_TipoDespacho` /`BFE_IndTraslado` |
| GET | `/catalogos/almacenes` | solo almacenes (filtros del listado) |
| GET | `/planificaciones?status=&whsCode=&desde=&hasta=&pagina=&tamano=` | **una página** (`{items,total,pagina,tamano,totalPaginas}` ), más nueva primero; filtros y salto en SAP. `tamano` máx. 100, por defecto 20 |
| GET | `/planificaciones/abierta` | la abierta (`{abierta: null}` si no hay; siempre JSON) |
| GET | `/planificaciones/:docEntry` | detalle: cabecera y líneas |
| POST | `/planificaciones/pegar` | `{whsCode, texto}` : valida Excel contra stock. No guarda |
| POST | `/planificaciones` | procesa: FIFO + guarda en `LIB_ODEV` . 409 si ya hay una abierta |
| PATCH | `/planificaciones/:id/cabecera` | serie, fecha, despacho, traslado, transporte |
| PATCH | `/planificaciones/:id/lineas/:lineId` | `{applied}` . 409 si la línea ya tiene `TargetEntry` |
| POST | `/planificaciones/:id/devoluciones` | crea las Devoluciones de las líneas marcadas |
| POST | `/planificaciones/:id/reprocesar` | rehace el FIFO: crea una planificación nueva y cierra la anterior. Solo si está **abierta y sin Devoluciones** (409 si no) |
| POST | `/planificaciones/:id/pdf` | `{unir?: boolean}` : pide a BES el PDF de la Guía de despacho (DTE 52) de cada Devolución; devuelve los archivos en base64 y el resultado por Devolución |
| POST | `/planificaciones/:id/cerrar` | cierra la planificación |
La sesión del Service Layer (`B1SESSION` /`ROUTEID` ) vive en el servidor; el navegador solo
tiene `dp_sid` . `SessionStore` es en memoria: alcanza para una sola instancia de la API y **se
pierde al reiniciar la API** (hay que volver a iniciar sesión).
### Metadatos (definitions/*.json)
- **Tablas:** `LIB_ODEV` (bott_Document) y `LIB_DEV1` (bott_DocumentLines).
- **UDO:** `LIB_ODEV` (boud_Document), hija `LIB_DEV1` . CanFind/CanClose/CanCancel = tYES,
CanDelete = tNO, CanLog = tYES (`ALIB_ODEV` ), sin formulario por defecto. Columnas de búsqueda:
`DocEntry` , `U_WhsCode` , `U_DocDate` .
- **Campos de `@LIB_ODEV` :** WhsCode, DocDate, Series (num), ObjType, PrintingLines (num),
TipoDespacho, IndicadorTraslado, RutTransporte, RutChofer, NombreChofer, FechaSalida,
HoraSalida (st_Time), FechaLlegada, Patente.
- **Campos de `@LIB_DEV1` :** LineType (F/S), ItemCode, ItemDescription, Quantity, OnHand, Status,
BaseType, BaseEntry, BaseLine, BaseRef, BaseDocDate, OpenQty, AllocatedQty, Applied (Y/N),
TargetType, TargetEntry, CardCode, CardName, ErrCode, ErrMsg.
- **Dependencias externas** (solo se verifican, no se crean): `OPDN.U_DOT_TipoRecepcion` ,
`OINV.U_BFE_TipoDespacho` , `OINV.U_BFE_IndTraslado` y la tabla `@DTE_CONFC` .
## Preparar el entorno (otro computador)
1. **Node 22 LTS** o superior (`node -v` ).
2. **Red con Fortinet** (inspección SSL): npm falla con `SELF_SIGNED_CERT_IN_CHAIN` .
Exportar antes de usar npm u ng:
```sh
export NODE_EXTRA_CA_CERTS="/ruta/a/fortinet-root.crt" # está en ../fortinet-root.crt
# alternativa, si el certificado está en el llavero de macOS:
export NODE_OPTIONS=--use-system-ca
export NG_CLI_ANALYTICS=false # si no, ` ng` puede quedar esperando el prompt
` ``
3. Instalar y configurar:
` ``sh
npm install
cp apps/api/.env.example apps/api/.env # completar SL_URL, SL_COMPANY_DB, DB_TYPE
` ``
Variables del ` .env` de la API: ` PORT` (3000), ` WEB_ORIGIN` (CORS), ` SL_URL` (con ` /b1s/v1`),
` SL_COMPANY_DB`, ` SL_REJECT_UNAUTHORIZED` (` false` si el certificado es autofirmado), ` DB_TYPE`
(` HANA`/` MSSQL`) y ` COOKIE_SECURE` (` true` solo con HTTPS). **El usuario y la contraseña de SAP no van
en el ` .env`**: se escriben en el login. Solo para producción: ` HTTPS_PFX_PATH`/` HTTPS_PFX_PASSPHRASE` o
` HTTPS_KEY_PATH`/` HTTPS_CERT_PATH`, ` HTTP_REDIRECT_PORT` y ` WEB_DIST_DIR` (opcional).
4. Desarrollo, en dos terminales:
` ``sh
npm run dev:api # compila @dp/shared y levanta NestJS en :3000
npm run dev:web # compila @dp/shared y levanta Angular en :4200 (proxy /api → :3000)
` ``
5. Verificación:
` ``sh
npm test # shared + api + web
npm run test:e2e -w api
npm run lint -w api
npm run build
npm run publicar # carpeta de despliegue en release/ (ver deploy/windows/DEPLOY.md)
` ``
Reglas:
- ` @dp/shared ` se consume **compilado** (` dist/`). Si se cambia, ejecutar ` npm run build:shared`
**y reiniciar ` ng serve`**: Vite sirve una copia vieja y la página falla con "does not provide an
export named ..." (los tests no lo detectan).
- En la API, los tipos que aparecen en firmas con decoradores se importan con ` import type`
(por ` isolatedModules` + ` emitDecoratorMetadata`). Los DTO (clases) sí se importan como valor.
- Al cambiar código del backend con ` start:dev` la API se reinicia y **se pierden las sesiones**.
- Los tests del frontend no usan zone.js: tras un ` flush` hay que esperar con
` await new Promise((r) => setTimeout(r))` y luego ` fixture.detectChanges()` (` whenStable` no basta).
- Para ver la UI sin la extensión de Chrome se puede abrir Chrome sin cabeza con
` --remote-debugging-port` y manejarlo por DevTools Protocol (` WebSocket` de Node 22).
## Estado al 2026-10-12
**Hecho y verificado contra el Service Layer real (SBOLibros, MSSQL):**
- Login, Metadatos y **Reconstruir** (creación de tablas, campos y UDO desde cero).
- ` libs/shared`: modelos y dominio portado del .NET. 14 tests.
- ` apps/api`: Service Layer, autenticación, metadatos, catálogos, planificaciones y PDF. 81 tests
unitarios + 6 e2e (incluye la cookie de sesión: ` HttpOnly`, ` Secure`, ` SameSite=Strict`), lint limpio. Prueba de extremo a extremo manual OK: pegar → procesar (FIFO) →
cabecera (fecha/hora) → marcar/desmarcar → crear Devolución (ORPD con base en OPDN) → ` TargetEntry`
grabado → cerrar. **Varios proveedores** OK contra SAP (una planificación con 4 líneas generó 3
Devoluciones, con un proveedor reuniendo dos líneas). Las reglas 409 (una sola abierta, línea con
destino, cerrada) OK. **Listado
paginado y filtrado** (total, saltos, estado/almacén/fechas) OK. **PDF** contra BES (certificación)
con 4 Devoluciones reales: individuales y unificado (3 páginas) OK.
- ` apps/web`: login, guards, Metadatos y Planificaciones completas (listado con filtros y paginación,
Nueva, Detalle con crear devoluciones, Rehacer FIFO, PDF y cerrar). 38 tests. Verificado en Chrome
real (sin cabeza): login, listado con filtros, selector de almacenes, validación de pegado y la
descarga de PDF del Detalle.
**Aprendido del Service Layer real:**
- **Una sesión no admite peticiones simultáneas**: las procesa en serie y cada una espera ~7 s tras
la anterior (seis en paralelo = ~40 s; una a una = ~40 ms). ` ServiceLayerClient.request` las
**encola por sesión**; no lo saltes con otro cliente HTTP. Avisa en el log si una tarda > 2 s.
- ` CanFind = tYES` exige ` UserObjectMD_FindColumns`; sin ellas SAP lo deja en ` tNO` en silencio.
- ` GET UserFieldsMD` informa los campos de hora como ` db_Numeric/st_Time` (el analizador lo normaliza).
- ` PATCH` de ` LIB_DEV1Collection` actualiza las filas por ` LineId` sin tocar las demás; **no se pueden
quitar filas** de un UDO.
- Los campos numéricos sin llenar del UDO vuelven como ` 0`: el mapeo los trata como ` null`.
- ` POST /LIB_ODEV(n)/Close` cierra el UDO. El UDO no se puede borrar (` CanDelete = tNO`).
- Si no hay resultado, el Service Layer responde cuerpo vacío: por eso ` /planificaciones/abierta`
devuelve siempre un JSON (` {abierta: null}`).
**Sin verificar:**
- **HANA**: el addon .NET ya funcionaba en HANA, y las consultas son un port de las suyas, así que
su lógica está probada. Lo que no se ha probado es **cómo las recibe el Service Layer de HANA**
(` SQLQueries`: su analizador es más estricto que el ` DoQuery` del addon; en MSSQL rechazó ` FORMAT`) y los
2026-10-13 14:30:06 -05:00
tres cambios respecto al addon: el filtro por almacén (` PDN1.WhsCode`), la columna ` PDN1.OpenInvQty`
y los parámetros con nombre (` :item`, ` :whs`). Tampoco el paginado (` $inlinecount`/` odata.count` es de ` /b1s/v1`; en ` /b1s/v2` sería
2026-10-13 08:09:07 -05:00
` $count`) ni los tipos que HANA informa en los metadatos.
- **Reprocesar** solo con tests (no contra SAP).
- Fallo parcial al crear Devoluciones (solo se probó el caso feliz). El corte en lotes de
` U_PrintingLines` líneas está probado con tests del algoritmo portado del addon, no contra SAP.
- El ` PATCH` de Reconstruir sobre un UDO ya existente con columnas de búsqueda.
- Paginación con varias páginas en la interfaz (hoy hay pocas planificaciones; sí está probada con la
API real y con tests de la pantalla).
- Recorrido con clics de **Procesar** en un navegador real.
- **PDF en la base de pruebas**: las Devoluciones de ` SBOLibros` no tienen ` FolioNum`, así que ahora su
descarga informa el error de folio. El PDF contra BES se probó antes del cambio, usando el ` DocNum`.
Para repetirlo hace falta una Devolución con folio (la que emite el addon de facturación electrónica).
- **Despliegue en Windows**: la carpeta publicada se probó en un Mac (API con HTTPS autofirmado, web
servida por la API, rutas de Angular, 404, cabeceras, redirección 301, log sin colores). **No se ha
2026-10-13 08:55:29 -05:00
ejecutado en Windows**: los ` .ps1` solo pasaron la comprobación de sintaxis y la lista de comandos de
NSSM que generan (` -SoloMostrar`); falta instalar el servicio de verdad. Las banderas de la cookie de sesión están comprobadas en un
2026-10-13 08:09:07 -05:00
test e2e; solo falta verlo en un navegador real sobre HTTPS (iniciar sesión en el sitio desplegado).
- **Estilos:** son CSS porque, en el Mac con macOS 12, el compilador nativo de Sass exige
macOS 14 o superior. En Sequoia se puede volver a SCSS si se prefiere.
## Siguientes pasos
1. **Validar en HANA** (ver "Sin verificar"): ` DB_TYPE=HANA`, Reconstruir, repetir la prueba de extremo a extremo.
2. Probar los casos sin verificar (fallo parcial y reprocesar contra SAP).
3. **Probar el despliegue en un servidor Windows** (de pruebas) siguiendo ` deploy/windows/DEPLOY.md`. El
botón de pegar desde el portapapeles solo funciona en ` localhost` o HTTPS.
4. Archivar ` ../SolucionLibros/` cuando HANA esté validado (ninguna carpeta está bajo ` git`).
## Preguntas abiertas para el usuario
1. ¿Se agregan UDF de fecha/hora de salida y llegada a la Devolución (ORPD)? Hoy no existen.
2. Tamaño de ` U_TipoDespacho`/` U_IndicadorTraslado` en ` @LIB_ODEV ` (10) frente a ORPD (1 y 2).
## Referencias en el addon .NET (` ../SolucionLibros/DevolucionProveedores/`)
Ya está todo portado; sirve para comparar resultados (sobre todo al validar en HANA).
- ` FormularioDevolucionProveedores.Negocio.cs`: parseo, consultas OnHand/FIFO, consolidación.
- ` FormularioDevolucionProveedores.Botones.cs`: Crear devoluciones (BOM y ` Add`), descarga de PDF.
- ` FormularioDevolucionProveedores.Bom.cs`: guardado del UDO (` GeneralService`) y BOM de la Devolución.
- ` WebServices/ConsultaDTE.cs`, ` WebServices/PdfUtils.cs`: SOAP BES y unión de PDF.
- ` ESPECIFICACION.md`: especificación original del proceso.
- ` bin/x64/Debug/UserFields.xml` (UTF-16): versión XML anterior de los metadatos, ya
reemplazada por los JSON de ` apps/api/src/metadata/definitions/`.