El servicio de Windows pasa a administrarse con NSSM (decisión del usuario: WinSW no se consideró lo bastante estable). - instalar-servicio.ps1: recibe -Nssm (ruta de nssm.exe) y configura el servicio con `nssm install` / `nssm set` (arranque automático, reinicio a los 10 s, logs a logs\ rotados a los 10 MB). Nueva opción -SoloMostrar que imprime los comandos sin ejecutarlos ni exigir administrador. Si un ajuste falla, quita el servicio a medio configurar. - desinstalar-servicio.ps1: usa `nssm stop` y `nssm remove`. - Se elimina la plantilla XML de WinSW. - DEPLOY.md: pasos con NSSM, sección para administrar el servicio, aviso de usar la compilación 2.24-101 en Windows 10/11 y Server 2019/2022 (la 2.24 estable puede no arrancar), y limpieza de logs rotados (NSSM no limita cuántos conserva). Ya no se requiere .NET Framework. - CLAUDE.md y README de la API actualizados. Sin probar aún en Windows: solo se comprobó la sintaxis y los comandos generados (-SoloMostrar). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
21 KiB
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):
- Pegar: ItemCode + cantidad desde Excel (con el botón Pegar desde el portapapeles o
Ctrl/Cmd+Ven el cuadro). Se valida el stock (OITW.OnHand) del almacén elegido. No guarda. - 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. - Crear devoluciones: agrupa las líneas marcadas sin
TargetEntrypor proveedor y las parte en lotes deU_PrintingLineslíneas (11 por defecto). Crea una Devolución por lote y grabaU_TargetType/U_TargetEntryen las líneas. - 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_Statusde cabecera eliminado. El estado usa elStatusestándar del UDO (O/C).- Se usan
U_TipoDespachoyU_IndicadorTrasladoen@LIB_ODEV. Se envían a la Devolución comoU_BFE_TipoDespacho/U_BFE_IndTraslado. - Cantidades y referencias son enteros (
db_Numeric):Quantity,OnHand,OpenQty,AllocatedQty,BaseEntry,BaseLine,BaseRef,TargetEntryyU_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
TargetEntryya no se puede desmarcar (verpuedeCambiarSeleccionenlibs/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 fallidasU_ErrCode/U_ErrMsgy se reintentan. - Consultas SQL por
SQLQueriesdel Service Layer (no hay conexión directa a la BD). La API crea/actualizaDP_StockyDP_EntradasAbiertassola (variante segúnDB_TYPE). El analizador SQL del Service Layer no admiteFORMATy SAP reescribe elSqlText: al cambiar el SQL hay que subirversionensql-queries.definitions.ts. Los parámetros van entre comillas simples. - 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.tsarma el sobre y lee la respuesta a mano yBesClientlo envía con axios (SOAPActionhttp://tempuri.org/Consulta_TXTDTE). Credenciales y URL salen deU_DTE_CONFCy nunca se registran ni se devuelven al navegador. Solo se consulta con elFolioNumberde 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 elDocNumcomo respaldo; se descartó porque podría traer el DTE de otro documento). Unir conpdf-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_PATHoHTTPS_KEY_PATH+HTTPS_CERT_PATH) y redirección opcional de HTTP (HTTP_REDIRECT_PORT). El servicio 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 publicararmarelease/devolucion-proveedores/(API + web + solo dependencias de producción, ~55 MB). Guía completa:deploy/windows/DEPLOY.md. - Logs: la aplicación no escribe logs a archivo; usa el
Loggerde Nest por consola. En producción NSSM captura la salida y la rota a los 10 MB (no limita cuántos archivos conserva: hay que limpiar los viejos). ConNO_COLOR=1el log sale sin códigos de 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)
├── deploy/windows/ NSSM: instalar/desinstalar-servicio.ps1, DEPLOY.md
└── 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) yLIB_DEV1(bott_DocumentLines). - UDO:
LIB_ODEV(boud_Document), hijaLIB_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_IndTrasladoy la tabla@DTE_CONFC.
Preparar el entorno (otro computador)
- Node 22 LTS o superior (
node -v). - Red con Fortinet (inspección SSL): npm falla con
SELF_SIGNED_CERT_IN_CHAIN. Exportar antes de usar npm u ng: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 - Instalar y configurar:
Variables del
npm install cp apps/api/.env.example apps/api/.env # completar SL_URL, SL_COMPANY_DB, DB_TYPE.envde la API:PORT(3000),WEB_ORIGIN(CORS),SL_URL(con/b1s/v1),SL_COMPANY_DB,SL_REJECT_UNAUTHORIZED(falsesi el certificado es autofirmado),DB_TYPE(HANA/MSSQL) yCOOKIE_SECURE(truesolo 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_PASSPHRASEoHTTPS_KEY_PATH/HTTPS_CERT_PATH,HTTP_REDIRECT_PORTyWEB_DIST_DIR(opcional). - Desarrollo, en dos terminales:
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) - Verificación:
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/sharedse consume compilado (dist/). Si se cambia, ejecutarnpm run build:sharedy reiniciarng 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(porisolatedModules+emitDecoratorMetadata). Los DTO (clases) sí se importan como valor. - Al cambiar código del backend con
start:devla API se reinicia y se pierden las sesiones. - Los tests del frontend no usan zone.js: tras un
flushhay que esperar conawait new Promise((r) => setTimeout(r))y luegofixture.detectChanges()(whenStableno basta). - Para ver la UI sin la extensión de Chrome se puede abrir Chrome sin cabeza con
--remote-debugging-porty manejarlo por DevTools Protocol (WebSocketde 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) →TargetEntrygrabado → 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.requestlas encola por sesión; no lo saltes con otro cliente HTTP. Avisa en el log si una tarda > 2 s. CanFind = tYESexigeUserObjectMD_FindColumns; sin ellas SAP lo deja entNOen silencio.GET UserFieldsMDinforma los campos de hora comodb_Numeric/st_Time(el analizador lo normaliza).PATCHdeLIB_DEV1Collectionactualiza las filas porLineIdsin 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 comonull. POST /LIB_ODEV(n)/Closecierra el UDO. El UDO no se puede borrar (CanDelete = tNO).- Si no hay resultado, el Service Layer responde cuerpo vacío: por eso
/planificaciones/abiertadevuelve 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 elDoQuerydel addon; en MSSQL rechazóFORMAT) y los dos cambios respecto al addon: el filtro por almacén (PDN1.WhsCode) y los parámetros con nombre (:item,:whs). Tampoco el paginado ($inlinecount/odata.countes de/b1s/v1; en/b1s/v2sería$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_PrintingLineslíneas está probado con tests del algoritmo portado del addon, no contra SAP. - El
PATCHde 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
SBOLibrosno tienenFolioNum, así que ahora su descarga informa el error de folio. El PDF contra BES se probó antes del cambio, usando elDocNum. 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
ejecutado en Windows: los
.ps1solo 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 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
- Validar en HANA (ver "Sin verificar"):
DB_TYPE=HANA, Reconstruir, repetir la prueba de extremo a extremo. - Probar los casos sin verificar (fallo parcial y reprocesar contra SAP).
- Probar el despliegue en un servidor Windows (de pruebas) siguiendo
deploy/windows/DEPLOY.md. El botón de pegar desde el portapapeles solo funciona enlocalhosto HTTPS. - Archivar
../SolucionLibros/cuando HANA esté validado (ninguna carpeta está bajogit).
Preguntas abiertas para el usuario
- ¿Se agregan UDF de fecha/hora de salida y llegada a la Devolución (ORPD)? Hoy no existen.
- Tamaño de
U_TipoDespacho/U_IndicadorTrasladoen@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 yAdd), 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 deapps/api/src/metadata/definitions/.