Files
basic-erp/CLAUDE.md
T

28 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.

Bancos (pagos recibidos y efectuados)

Menú Bancos → /bancos/pagos-recibidos (IncomingPayments) y /bancos/pagos-efectuados (VendorPayments), en apps/web/src/app/features/banking/. Medios: efectivo, transferencia y cheques; se aplican a facturas/notas de crédito abiertas del socio, a cuenta (lo que sobra de los medios queda como anticipo, implícito) o a cuentas contables (rAccount). Un pago contabilizado no se edita: se consulta y se anula (POST Recurso(clave)/Cancel, acción en la lista blanca de sl-resources.ts, auditada como sl_action).

Reglas de SAP verificadas: nota de crédito se aplica con SumApplied negativo; los cheques exigen CheckAccount en la cabecera; pagar de más trunca en silencio (se valida en el cliente); solo moneda local (localCurrency de la pantalla, hoy CLP). En SBODemoCL los pagos efectuados a facturas fallan por falta de la cuenta de "diferencias de conversión realizadas" (configuración de SAP); los de rAccount funcionan. Pendiente: depósitos y conciliación bancaria, moneda extranjera.

Finanzas (plan de cuentas y asientos)

Menú Finanzas: Plan de cuentas (/finanzas/plan-de-cuentas) es un maestro más (masters/finanzas/, recurso ChartOfAccounts, clave Code; extensiones por cliente en finanzas.extensions.ts) y Asientos contables (/finanzas/asientos, features/finance/, recurso JournalEntries, clave JdtNum) tiene lista y formulario propios (journal-model.ts: cuadre debe = haber, líneas con cuenta, dimensiones y proyecto, referencias 1 a 3, indicador y folio; SAP ignoró el folio en el asiento de prueba). Verificado en SAP: en una cuenta ya creada solo se puede cambiar el nombre y similares; ActiveAccount (de movimiento / de título) y la cuenta padre no se cambian (campos immutable), y congelarla exige un rango de fechas (no se ofrece). Un asiento no se modifica; solo los manuales (OriginalJournal = ttJournalEntry) se cancelan con Cancel, que genera el asiento de reverso (el original no lleva marca de cancelado); los de documentos se cancelan con su documento. El plan de cuentas se abre como árbol (chart-tree.page → chart-tree.panel): pestañas verticales por grupo (las cuentas de primer nivel), selector «Nivel», azul = título, verde = asociada; las hijas de cada cuenta se piden al desplegarla y buscar consulta al servidor (un plan segmentado tiene miles de cuentas). Al elegir una cuenta su formulario queda a la izquierda y el árbol a la derecha (MasterLayout.chartTree), como SAP. Campos del formulario según SAP (verificado con el usuario): «Cuenta asociada» = LockManualTransaction, «Cuenta monetaria» = CashAccount, «Bloquear contabilización manual» = BlockManualPosting, «Confidencial» = Protected; pestañas Propiedades, Contabilidad de costos e Info detallada (sin DefaultVatGroup ni VATRegNum). Plan de cuentas segmentado (verificado en SBO_TAGUMEDICA_21, HANA, 4.470 cuentas): las cuentas de movimiento tienen Code interno (_SYS00000004311) y FormatCode visible (8911111010); las de título tienen Code corto y FormatCode vacío; en un plan sin segmentar FormatCode = Code. El Service Layer no expone la bandera CINF.EnbSgmnAct (AdminInfo solo trae AccountSegmentsSeparator), así que no se detecta: todo muestra displayCode = FormatCode (con los separadores de AccountSegmentations, tamaños 6-2-2 aquí) o, si falta, Code, y la clave que viaja a SAP es siempre Code. Líneas del asiento: columnas «Cuenta / socio» (una cuenta o un cliente/proveedor; el «…» pregunta cuál lista abrir) → «Nombre» → «Cuenta». Si es un socio, «Cuenta» es su cuenta de control (se completa sola con DebitorAccount del socio, pedida aparte porque listarla junto con los socios da 500 en HANA) y su lista solo trae cuentas asociadas (LockManualTransaction = tYES); si es una cuenta, se repite en «Cuenta» y no se edita. Las celdas de cuenta muestran el código con formato y aceptan escribirlo (con o sin separadores); se guarda el Code. Un asiento nuevo arranca con todas las fechas de hoy (hora local, todayIso); lo que se cambia en la cabecera (vencimiento, comentarios, referencias 1 a 3) se ofrece a las líneas («Sí, actualizar / No, dejarlas») y las líneas nuevas lo toman; el camino inverso no existe. Las ventanas de selección cargan 100 filas por página. Pendiente: libro mayor y saldos, asientos recurrentes, crear cuentas en un plan segmentado (formato del código sin verificar; no hay permiso de escritura en esa base) y avisar al escribir un código de cuenta inexistente en las celdas del asiento.

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 ~289 kB, 82 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.
  • Pruebas con otra persona en la red: npm run dev:web:lan abre la web a la red (http://<IP del equipo>:4200; el proxy /api llega al backend local). El backend (:3000) escucha en todas las interfaces y la web normal (dev:web) solo en localhost; ambos son de desarrollo, sin HTTPS. La otra persona necesita su propio usuario web (U_WEB_USER/U_WEB_PASS en EmployeesInfo) y permisos de menú (/admin/permisos): hay una sesión por usuario, así que no debe entrar con el mismo que tú.
  • 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. SBO_TAGUMEDICA_21 (HANA, plan de cuentas segmentado) es de un cliente: no se escribe nada ahí sin preguntar antes al usuario (crear, actualizar, cancelar, consultas guardadas, campos de usuario); las consultas son libres.
  • 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) · formularios de todos los documentos y maestros · auditoría · Bancos (pagos recibidos y efectuados) · Finanzas (plan de cuentas y asientos).

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: 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 al final y se envían a SAP) y listColumns. Además de agregar, la extensión puede ocultar (hideHeaderFields, hideLineColumns, hideListColumns; en maestros hideFields, hideListColumns) y dejar de solo lectura (readOnlyHeaderFields, readOnlyLineColumns; en maestros readOnlyFields): lo oculto ni se muestra ni se envía; lo de solo lectura se ve pero no se edita y su valor sigue enviándose (columna locked, a diferencia de readonly, que no se envía). Los nombres son los de SAP (CostingCode2, Indicator…); un nombre inexistente, ocultar un campo/columna obligatorio, o una ruta inexistente o ajena al módulo lanzan 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). La alineación de una columna se declara con align: 'left' | 'center' | 'right' (listas y detalle; por defecto, derecha en las numéricas).

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. Campos de contabilidad analítica y numeración (estándar): cabecera de todos los documentos: Series (la fila «Número» lleva la lista de series del documento, pedida con la acción SeriesService_GetDocumentSeries del Service Layer —registrada en el proxy como acción de solo consulta—, filtrada por el subtipo de la pantalla; no se puede cambiar una vez creado; sin elegir, SAP usa la por defecto), Indicator (ventana de selección sobre FactoringIndicators —tabla OIDC; en tu SAP: 33 Factura, 39 Boleta; el recurso Indicators no existe en este Service Layer) y Project (lista de proyectos). Detalle de ventas y compras: PriceAfterVAT (el precio con IVA; solo se envía a SAP el que el usuario escribió, Price o PriceAfterVAT, nunca los dos), CostingCode a CostingCode5 y ProjectCode (cada uno con su ventana modal de selección: reglas de distribución por dimensión y proyectos), AccountCode (plan de cuentas) y BaseType/BaseEntry/BaseLine (solo lectura: el documento de origen de una línea copiada). Inventario y solicitudes de compra llevan en el detalle solo las cinco dimensiones y el proyecto. OBJECT_TYPES (document-objects.ts) da el tipo de objeto de SAP de cada pantalla. El subtipo de las series de boleta (IB) y el resto de SERIES_SUBTYPE no está verificado contra SBODemoCL (solo hay series --).

Movimientos de inventario: cabecera con fecha de contabilización, fecha del documento (TaxDate), vencimiento (DueDate, solo traslados), Reference2 y comentarios del asiento (JournalMemo); Reference1 lo asigna SAP. Copiar de/a: la transferencia de stock se crea desde una solicitud de traslado (COPY_FLOWS); en las líneas de transferencia BaseType es un enumerado de texto de SAP (InventoryTransferRequest, WarehouseTransfers…; STOCK_BASE_TYPES, hasBaseRef), no el número de objeto de ventas y compras.

Lotes y series (entradas, salidas y transferencias de stock): si el artículo se gestiona por lote (ManageBatchNumbers) o por serie (ManageSerialNumbers; el maestro de artículos los trae con extraFields al elegirlo) la línea muestra un botón «Indicar lotes / series» (columna «Lotes / Series») que abre una ventana con una tabla editable: lotes = BatchNumber, Quantity, ExpiryDate, ManufacturingDate; series = InternalSerialNumber, ManufacturerSerialNumber, fechas (cada serie con Quantity: 1). Viajan dentro de la línea como BatchNumbers / SerialNumbers; solo se envían los campos propios (no los que SAP agrega al leer). Validación: los lotes deben sumar la cantidad de la línea y debe haber una serie por unidad. Al cambiar de artículo se descartan. Un documento contabilizado los muestra en solo lectura. Las solicitudes de traslado, las de compra y los documentos de ventas/compras no los llevan todavía (layout.details, LINE_DETAILS en line-details.ts). Probado: entrada de inventario 156 (1 unidad de SU01 en el almacén 01, lote LT-PRUEBA-01). Pendiente: elegir de una lista los lotes existentes en una salida o transferencia (hoy se escribe el número; SAP valida que exista y haya saldo).

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 — hechas (clientes, proveedores y artículos): features/masters/, con la misma idea que los documentos pero con clave de texto (CardCode, ItemCode). Cada pantalla declara su MasterScreen (socios-negocio/socios.screens.ts, articulos/articulos.screens.ts): columnas y filtros de la lista, baseFilter (clientes = CardType eq 'cCustomer', proveedores = cSupplier; el tipo también se envía al crear con fixed), y un MasterLayout con código/nombre/estado arriba y los demás campos en pestañas (dos columnas, tipos de HeaderField: texto, número, fecha, área de texto, casilla tYES/tNO, lista de valores numérica o de texto, almacén). El código no se cambia una vez creado (immutable); «Inactivo» es Frozen. Rutas con master-paths.ts + resolve diferido; navegación primero/anterior/siguiente/último por clave de texto (SlMasterApi). Los campos por cliente van en socios-negocio/socios.extensions.ts y articulos/articulos.extensions.ts (vacíos en main; header, tabs por id, newTabs, listColumns, y también hideFields, readOnlyFields, hideListColumns; pestaña, campo o ruta inexistente = error). Tablas en las pestañas de los maestros (MasterTab.collection, MasterCollection; componente sbo-rows-table): clientes y proveedores tienen Direcciones (BPAddresses) y Personas de contacto (ContactEmployees); el artículo, Precios (ItemPrices) y Stock por almacén (ItemWarehouseInfoCollection, solo lectura). Cómo se envían (verificado contra SAP con un cliente y un artículo de prueba): mode: 'replace' (direcciones, contactos) manda la colección completa —las filas leídas con los cambios aplicados, para conservar sus campos de SAP (RowNum, InternalCode…), más las nuevas— con B1S-ReplaceCollectionsOnPatch: true (así también se puede quitar una fila, si SAP lo permite); mode: 'merge' (precios) manda solo las filas cambiadas, por rowKey (PriceList) y sin reemplazo, porque reenviar las demás listas con valores viejos impide que SAP recalcule las derivadas por factor. Las filas vacías (sin identity) no se guardan. Probado: cliente TESTBP01 (dirección de envío añadida) y artículo TESTITEM01 (precio de la lista 3), ambos de prueba en SBODemoCL. Pendiente en maestros: las extensiones de cliente aún no pueden agregar u ocultar columnas de estas tablas, ni cambiar el tipo de socio. Los catálogos BusinessPartnerGroups e ItemGroups están registrados en el proxy. 3. Pantalla web de permisos — hecha (/admin/permisos). 4. Auditoría — en marcha (apps/api/src/audit/, tabla audit_log en la misma SQLite): registra inicios de sesión (correctos, fallidos con su motivo —credenciales, usuario inactivo, demasiados intentos— y cierres), cada creación y modificación que pasa por el proxy /api/sl (recurso, clave, resultado ok/error con el mensaje de SAP, y solo los nombres de los campos y cuántas filas lleva cada colección: nunca valores ni contraseñas), las operaciones denegadas por permisos y los cambios de permisos de menú. Las lecturas y las acciones de solo consulta (series) no se auditan. AuditService.record nunca lanza (un fallo del registro no cambia la operación del usuario). Consulta solo para administradores: GET /api/audit (filtros usuario, acción, recurso, resultado, desde/hasta; 50 por página, máx. 200) y la pantalla /admin/auditoria (menú Administración, solo acceso total). Pendiente de afinar: retención (purgeOlderThan existe, falta programarlo y su variable de entorno), la IP detrás del proxy de desarrollo (todo llega como loopback; hace falta xfwd en el proxy y trust proxy solo cuando el backend no esté expuesto), exportar a CSV, y nestjs-cls para poner el usuario en cada línea del log. 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.