Files
basic-erp/CLAUDE.md
T

15 KiB
Raw Blame History

basic-erp

ERP base para SAP Business One (Service Layer). Migración del mini-erp (React + FastAPI, en /Users/george/Documents/Desarrollo/React/mini-erp) a NestJS + Angular, ambos en TypeScript. Sirve de base para crear una rama por cliente.

Monorepo npm workspaces:

  • apps/api — NestJS 12 (ESM): proxy hacia el Service Layer, login, sesión SAP compartida, permisos de menú (SQLite). Ver apps/api/README.md.
  • apps/web — Angular 22 (standalone, zoneless, señales): componentes reutilizables estilo SAP Business One. Ver apps/web/README.md.
  • libs/sap-types — tipos y utilidades compartidos por api y web (MENU, evaluador de permisos, tipos de auth/SL). Se compila en postinstall.
  • tools/ — mockup oferta_venta.html (ejemplo: tiene algunos campos no estándar, pero se conserva la idea).

Reglas del proyecto base

  • Solo campos estándar de SAP Business One. Ningún campo de usuario (U_*) salvo U_WEB_USER / U_WEB_PASS de EmployeesInfo, usados para el login web.
  • Los formularios se construyen con componentes reutilizables y configuración declarativa; no se duplica markup por documento.
  • Aspecto por variables CSS (temas). Tema por defecto: claro, con la paleta del mockup. Esquinas contenidas (--radius*, como SBO).
  • La fila de un formulario es etiqueta + uno o varios controles (input, select, número, fecha, botón); la cabecera tiene como máximo 2 columnas.
  • Impuesto de las líneas: TaxCode con el maestro SalesTaxCodes (verificado en el Service Layer; VatGroups no tiene Rate). Un cliente con VatGroup lo cambia en su rama.

Criterios de la interfaz

Es un ERP, no una página de publicidad: ligera y rápida. Sin librerías de componentes (Material/PrimeNG), sin webfonts ni animaciones, componentes propios y densos (13 px), zoneless + OnPush, rutas con carga diferida, listas largas paginadas en el servidor (50 por página). Presupuesto del bundle inicial en angular.json (aviso 300 kB; hoy ~286 kB, 81 kB comprimido; la configuración de las pantallas de documento se carga en diferido con un resolve, y document-paths.ts es la única lista que el enrutador necesita al arrancar: httpResource aporta ~18 kB al chunk compartido; si hace falta recuperarlos, reemplazarlo por RxJS).

Comandos

  • npm install — npm 11 (npm 10 falla al resolver las dependencias de Nest 12: npx npm@11 install); instala todo y compila libs/sap-types.
  • npm run dev:api (:3000) · npm run dev:web (:4200, proxy /api) · npm run build · npm test.
  • Backend contra SAP real (opt-in): SAP_LIVE=1 npm run test:e2e -w apps/api.
  • Página de componentes solo desarrollo: http://localhost:4200/dev/componentes (toolbar, ventana, campos, pestañas, grilla y listas de valores con datos reales; no guarda nada).

Decisiones tomadas (con su porqué)

  • Login web con U_WEB_USER/U_WEB_PASS (se mantiene). Mismo mensaje para usuario inexistente y clave errónea; comparación en tiempo constante; 429 tras 5 fallos por usuario / 20 por IP; una sesión por usuario; timeout deslizante 30 min (el servidor informa sessionTimeoutMinutes); token Authorization: Bearer.
  • Usuario SAP compartido (hoy Manager de pruebas; luego un usuario de licencia indirecta). Todo sale auditado con ese usuario.
  • Concurrencia hacia el Service Layer = 1 (SAPB1_SL_MAX_CONCURRENCY): medido en SAP10 (MSSQL, 32 GB), 12 peticiones tardan 0.6 s en serie y 7.5 s con 2 en paralelo. Un pool de sesiones no ayuda; la cola sí. Repetir la medición en cada cliente.
  • TLS: SAPB1_SL_CA_FILE opcional (certificado autofirmado de SAP o CA de firewall); VERIFY_SSL=true por defecto y no se puede apagar en producción. fortinet.cer no sirve para SAP10: presenta su propio certificado autofirmado (apps/api/certs/sap10.pem, ignorado por git).
  • Proxy /api/sl/:recurso: el frontend arma $select/$filter/...; el backend valida y ejecuta solo recursos registrados (apps/api/src/sl/sl-resources.ts), con permiso de menú y cabeceras B1S-* de lista blanca. Nunca registrar EmployeesInfo (tiene U_WEB_PASS). InventoryRequests no existe: es InventoryTransferRequests.
  • Permisos de menú en SQLite (SQLITE_PATH), patrones *, /ventas/*, /ventas/ordenes; ADMIN_WEB_USERS garantiza el acceso total; no se puede quitar el último admin.
  • Listas de documentos: más recientes primero (DocEntry desc), 50 por página, Estado = Abierto por defecto (como el mini-erp; cambiarlo es un defaultValue), filtro de fecha siempre rango [desde – hasta], multi-selección con chips, búsqueda numérica por igualdad. Sin filtro base en las demás listas; las de Invoices filtran por subtipo exacto: cuatro pantallas comparten el recurso y se separan por DocumentSubType (enum BoDocumentSubType: bod_None 0, bod_InvoiceExempt 1, bod_DebitMemo 2, bod_Bill 3, bod_ExemptBill 4, bod_PurchaseDebitMemo 5, bod_ExportInvoice 6…) y por ReserveInvoice (tYES = de reserva): Facturas (tNO y eq bod_None), Boletas (tNO y eq bod_Bill), Facturas de reserva (tYES y eq bod_None) y Boletas de reserva (tYES y eq bod_Bill); baseFilter para la lista y fixed (subtipo, y ReserveInvoice en las reservas) al crear. Nunca ne: los demás subtipos (factura exenta, nota de débito, boleta exenta, exportación) no se mezclan y tendrán su propia pantalla si se necesitan. baseFilter de la pantalla se aplica también a la navegación primero/anterior/siguiente/último. El PDF y los reportes se migran después vía API Gateway (otro servicio, con su propia cola y sesión).
  • Reportes/PDF no se generan en la base; el botón PDF queda pendiente hasta migrar el API Gateway.

Flujo de las pantallas de documento

Cada menú abre primero una lista de los últimos documentos (barra de filtros + búsqueda por columna); desde ahí se crea uno nuevo (/…/nuevo, formulario vacío) o se abre uno existente (/…/:docEntry, ver/editar; los cerrados/cancelados en solo lectura). Las pantallas se definen por módulo en apps/web/src/app/features/documents/ (17 documentos): ventas/ventas.screens.ts, compras/compras.screens.ts (+ compras.layouts.ts) e inventario/inventario.screens.ts (+ inventario.layouts.ts); los maestros (socios de negocio, artículos) irán en su propia carpeta. screen-kit.ts tiene el tipo DocumentScreen y las columnas y filtros comunes; document-screens.ts las reúne en DOCUMENT_SCREENS. Las columnas de línea estándar están en features/documents/line-columns.ts.

Cómo verificar cambios de interfaz

  • Probar contra el SAP real con la extensión de Chrome (list_connected_browsers debe mostrar un navegador). Si no conecta, usar Chrome headless por CDP.
  • No escribir contraseñas en formularios: obtener el token con curl a /api/auth/login (la clave de prueba se lee de EmployeesInfo solo para eso, nunca se imprime ni se guarda) e inyectar solo el token en localStorage (erp_session).
  • Al terminar: cerrar la sesión de prueba (POST /api/auth/logout), borrar erp_session/erp_theme del navegador, cerrar la pestaña, apagar los servidores y borrar archivos temporales (token, apps/api/data/). No dejar páginas de demostración fuera de /dev/componentes.
  • Escrituras a SAP (POST/PATCH): el usuario autorizó (2026-10-14) escribir en SBODemoCL, su ambiente propio de pruebas; ya se probaron crear/actualizar órdenes de venta y de compra, reemplazo de líneas y el 412 por conflicto. Documentos de prueba creados: orden de venta 315 (cerrada, copiada a la entrega 580) y orden de compra 2883 (cerrada, copiada a la recepción 299). Aun así, avisar antes de escribir en cualquier otra base.
  • Los tests no detectan todo: el foco tras crear una línea (se pedía antes de que existiera la fila) solo falló en el navegador real.

Estado y próximos pasos

Hecho (commits en main): monorepo · API (config, capa SAP con sesión compartida y cola, login, permisos SQLite, proxy /api/sl) · web (temas, login, layout con menú por permisos, sbo-value-help, campos y filas compuestas, listas con filtros, ventana/toolbar/pestañas/menús, grilla de líneas, /dev/componentes).

Pendiente, en orden:

  1. Paso 9 — formulario de documentos: hecho para ventas y compras (document-form.page, document-model.ts: cargar, crear, actualizar con If-Match/412, navegación, totales con SalesTaxCodes (también en compras), solo lectura), probado con SAP simulado y contra SBODemoCL (leer, crear, actualizar, líneas, 412). Un código escrito a mano (socio, artículo, almacén, impuesto) se resuelve al salir del campo con SlLookupApi (mismo recurso y filtro base que la lista "…"); si no existe se avisa. "Copiar de/a" hecho (document-copy.ts: flujos estándar y BaseType/BaseEntry/BaseLine; probado: orden de compra 2883 → recepción 299, orden de venta 315 → entrega 580, ambas órdenes quedaron cerradas). Formularios sin socio ni totales hechos (document-layouts.ts, configuración declarativa: cabecera de 2 columnas, columnas de línea, colección DocumentLines o StockTransferLines): entradas y salidas de inventario, transferencias de stock, solicitudes de traslado y solicitudes de compra. Entradas, salidas y transferencias contabilizadas son de solo lectura (SAP no las modifica); solicitudes de traslado y de compra se crean y modifican. Probado contra SBODemoCL: solicitud de compra 2 (la 1 es una prueba por API), solicitud de traslado 472 (cantidad modificada) y entrada de inventario 155 (1 unidad de 3MG30080714 M 10.5 en el almacén 8803). Limitaciones conocidas: los artículos por lote o serie no se pueden contabilizar (SAP pide BatchNumbers/SerialNumbers); las líneas de compra/inventario llevan UoMEntry solo si el artículo tiene grupo de unidades (uomEntryOf; sin él SAP responde "specify a UoM code"); los formularios de ventas y compras también lo envían (la unidad de venta o de compra del artículo, o la de inventario; itemColumnWithUom), y no se envía la unidad manual (-1). Probado: orden de compra 2884 con SU01, que no tiene unidad de compra por defecto. Cómo se define un formulario: el formulario base (document-form.page) no sabe de qué documento se trata; recibe de la pantalla su layout (DocumentLayout en document-layouts.ts): header y footer (cada uno con left/right, máximo 2 columnas) son arreglos de campos tomados del catálogo FIELDS (texto, fecha, número, almacén, socio, folio, calculado), columns son las columnas de línea y lines la colección que se guarda. Cada pantalla (en la carpeta de su módulo) declara los suyos (la orden de venta no lleva folio; las facturas, notas de crédito, entregas y recepciones sí, con TAX_DOCUMENT); agregar o quitar un campo es editar ese arreglo, sin tocar el HTML. En ventas y compras la pantalla puede agregar campos (headerLeft/headerRight), reemplazar la cabecera (header) o elegir sus columnas de línea del catálogo LINE_COLUMNS (lineColumns: [LINE_COLUMNS.item, LINE_COLUMNS.quantity, …]; lo que se envía por línea sale de ellas); sin elegir, el documento lleva la cabecera y todas las columnas base.

Campos por cliente (estándar adicionales o U_*): no se editan las pantallas base. Cada módulo tiene su archivo de extensiones, vacío en main: ventas/ventas.extensions.ts, compras/compras.extensions.ts e inventario/inventario.extensions.ts (reunidos en client-extensions.ts). La rama cliente/<nombre> declara ahí, por ruta de pantalla, qué suma: headerLeft/headerRight/footerLeft/footerRight (campos; tipos text, date, number, textarea, select con options, warehouse…), lineColumns (se agregan antes del total y se envían a SAP) y listColumns. Solo suma, no quita; una ruta inexistente o ajena al módulo lanza un error al arrancar. El test «el proyecto base no usa campos de usuario» revisa BASE_SCREENS, no las extensiones. Verificado: el proxy /api/sl deja pasar nombres U_* en $select/$orderby y en el cuerpo (el error de un campo inexistente lo da SAP). Límite: una columna de línea de usuario con lista de valores válidos aún no tiene editor desplegable (solo texto/número).

Columnas de las ventanas de selección (cliente, artículo, almacén, impuesto): son configurables por documento. En una pantalla del base (ventas/compras): lookups: { item: [...], partner: [...], warehouse: [...], tax: [...] } en el último parámetro de marketing(...) sustituye las columnas de esa lista (la primera columna es el código que se escribe en el campo). En la rama del cliente: lookupColumns en su archivo de extensiones suma columnas al final (por ejemplo U_Marca en artículos). El recurso, el filtro y el orden de cada lista no cambian; para los documentos de inventario o solicitudes se usa withLookupColumns(layout, cols, 'replace' | 'add'). Las columnas de almacén aplican también al almacén de la cabecera y al de origen de una transferencia. Los formularios con socio muestran NumAtCard y el RUT (FederalTaxID, solo lectura: lo completa SAP desde el socio); facturas, notas de crédito, entregas y recepciones (FOLIO_PATHS en document-screens.ts) añaden el folio tributario, que en el Service Layer es FolioPrefixString + FolioNumber (se edita como texto y se envía como número); las listas de facturas también los muestran. Flete hecho: fila «Flete» en el pie de ventas y compras (gasto adicional + importe; freightPayload lo guarda como DocumentAdditionalExpenses, reemplazando la colección; el catálogo AdditionalExpenses está registrado en el proxy; con varios gastos adicionales cargados en SAP solo se muestra). freight: false en marketing(...) lo quita de un documento. El toolbar del formulario ocupa todo el ancho del contenido (<sbo-toolbar bleed>). Falta: botón PDF. En ventas el flete falla en SBODemoCL ("Define the freight revenue account…"): el gasto adicional «Flete» solo tiene cuenta de gasto, no de ingresos (configuración de SAP, no del código); en compras funciona (orden de compra 2885). 2. Pantallas de maestros (Items, BusinessPartners: Clientes/Proveedores, Artículos), hoy "no disponible". 3. Pantalla web de permisos — hecha (/admin/permisos). 4. Auditoría (audit_log en SQLite; hoy solo líneas AUDIT en el log) y nestjs-cls para el usuario en los logs. 5. Migrar reportes/PDF vía API Gateway (módulo y cola aparte; no registrar credenciales de los tools/ del mini-erp, traen contraseñas en claro). 6. Revisar las vulnerabilidades de npm audit y definir una política para U_WEB_PASS (texto plano, claves cortas) antes de producción. 7. Docker/despliegue (HTTPS tras Fortinet; sesiones web en memoria ⇒ un solo proceso).

Flujo de trabajo por cliente (branches)

  1. main es la versión base/genérica, sin personalizaciones de cliente.
  2. Cada cliente tiene su branch (cliente/<nombre-cliente>); ahí viven sus personalizaciones y fixes.
  3. Los cambios de una branch de cliente no se mezclan con main ni con otro cliente, salvo backport genérico decidido explícitamente.
  4. Antes de crear branches, mergear a main, o hacer merge/backport entre clientes, confirmar con el usuario. No hacer git push sin confirmación. Cada paso termina en un commit propio.