14 KiB
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). Verapps/api/README.md.apps/web— Angular 22 (standalone, zoneless, señales): componentes reutilizables estilo SAP Business One. Verapps/web/README.md.libs/sap-types— tipos y utilidades compartidos por api y web (MENU, evaluador de permisos, tipos de auth/SL). Se compila enpostinstall.tools/— mockupoferta_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_*) salvoU_WEB_USER/U_WEB_PASSdeEmployeesInfo, 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:
TaxCodecon el maestroSalesTaxCodes(verificado en el Service Layer;VatGroupsno tieneRate). Un cliente conVatGrouplo 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 compilalibs/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 informasessionTimeoutMinutes); tokenAuthorization: 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_FILEopcional (certificado autofirmado de SAP o CA de firewall);VERIFY_SSL=truepor defecto y no se puede apagar en producción.fortinet.cerno 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 cabecerasB1S-*de lista blanca. Nunca registrarEmployeesInfo(tieneU_WEB_PASS).InventoryRequestsno existe: esInventoryTransferRequests. - Permisos de menú en SQLite (
SQLITE_PATH), patrones*,/ventas/*,/ventas/ordenes;ADMIN_WEB_USERSgarantiza 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 undefaultValue), filtro de fecha siempre rango [desde – hasta], multi-selección con chips, búsqueda numérica por igualdad. Sin filtro baseDocumentSubTypesalvo las boletas: la boleta de venta es unaInvoicesconDocumentSubType = bod_Bill(pantalla/ventas/boletas,baseFilter+fixedal crear); la lista de facturas solo excluyebod_Bill(ne, noeq bod_None, para no perder notas de débito u otros subtipos). En SBODemoCL haybod_Noneybod_Bill.baseFilterde 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_browsersdebe mostrar un navegador). Si no conecta, usar Chrome headless por CDP. - No escribir contraseñas en formularios: obtener el token con
curla/api/auth/login(la clave de prueba se lee deEmployeesInfosolo para eso, nunca se imprime ni se guarda) e inyectar solo el token enlocalStorage(erp_session). - Al terminar: cerrar la sesión de prueba (
POST /api/auth/logout), borrarerp_session/erp_themedel 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:
- Paso 9 — formulario de documentos: hecho para ventas y compras (
document-form.page,document-model.ts: cargar, crear, actualizar conIf-Match/412, navegación, totales conSalesTaxCodes(también en compras), solo lectura), probado con SAP simulado y contraSBODemoCL(leer, crear, actualizar, líneas, 412). Un código escrito a mano (socio, artículo, almacén, impuesto) se resuelve al salir del campo conSlLookupApi(mismo recurso y filtro base que la lista "…"); si no existe se avisa. "Copiar de/a" hecho (document-copy.ts: flujos estándar yBaseType/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ónDocumentLinesoStockTransferLines): 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 contraSBODemoCL: solicitud de compra 2 (la 1 es una prueba por API), solicitud de traslado 472 (cantidad modificada) y entrada de inventario 155 (1 unidad de3MG30080714 M 10.5en el almacén 8803). Limitaciones conocidas: los artículos por lote o serie no se pueden contabilizar (SAP pideBatchNumbers/SerialNumbers); las líneas de compra/inventario llevanUoMEntrysolo 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 conSU01, 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 sulayout(DocumentLayoutendocument-layouts.ts):headeryfooter(cada uno conleft/right, máximo 2 columnas) son arreglos de campos tomados del catálogoFIELDS(texto, fecha, número, almacén, socio, folio, calculado),columnsson las columnas de línea ylinesla 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í, conTAX_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álogoLINE_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.
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)
maines la versión base/genérica, sin personalizaciones de cliente.- Cada cliente tiene su branch (
cliente/<nombre-cliente>); ahí viven sus personalizaciones y fixes. - Los cambios de una branch de cliente no se mezclan con
mainni con otro cliente, salvo backport genérico decidido explícitamente. - Antes de crear branches, mergear a
main, o hacer merge/backport entre clientes, confirmar con el usuario. No hacergit pushsin confirmación. Cada paso termina en un commit propio.