33 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.
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). El menú lateral se oculta con el botón ☰ (en pantallas anchas gana ancho para formularios y tablas y se recuerda en localStorage erp_sidebar; en estrechas se abre sobre el contenido); cada módulo además se pliega por su cuenta.
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.- Pruebas con otra persona en la red:
npm run dev:web:lanabre la web a la red (http://<IP del equipo>:4200; el proxy/apillega al backend local). El backend (:3000) escucha en todas las interfaces y la web normal (dev:web) solo enlocalhost; ambos son de desarrollo, sin HTTPS. La otra persona necesita su propio usuario web (U_WEB_USER/U_WEB_PASSenEmployeesInfo) 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 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 base en las demás listas; las deInvoicesfiltran por subtipo exacto: cuatro pantallas comparten el recurso y se separan porDocumentSubType(enumBoDocumentSubType:bod_None0,bod_InvoiceExempt1,bod_DebitMemo2,bod_Bill3,bod_ExemptBill4,bod_PurchaseDebitMemo5,bod_ExportInvoice6…) y porReserveInvoice(tYES= de reserva): Facturas (tNOyeq bod_None), Boletas (tNOyeq bod_Bill), Facturas de reserva (tYESyeq bod_None) y Boletas de reserva (tYESyeq bod_Bill);baseFilterpara la lista yfixed(subtipo, yReserveInvoiceen las reservas) al crear. Nuncane: 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.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. Tipo Artículo / Servicio (DocType): los documentos de ventas y compras con socio llevan en la cabecera la lista «Tipo» (primera de la columna izquierda; no se cambia en uno guardado). Con «Servicio» las líneas pasan a layout.service (serviceLineColumns en line-columns.ts): Cuenta contable (obligatoria), Descripción, Impuesto y Total (se escribe, sin recalcular), dimensiones, proyecto y referencias al origen; sin artículo, cantidad, almacén, precio, lotes ni «Elegir artículos». Se guardan AccountCode, ItemDescription, TaxCode, LineTotal (LinesSpec.service); cambiar el tipo con líneas escritas pide permiso y las descarta; «Copiar de» trae el tipo y el importe abierto (OpenAmount) del origen y no mezcla tipos. Las extensiones de cliente de columnas de línea aplican a las líneas de artículos; las de servicio aún no son extensibles. Indicador y folio juntos y en ese orden (IDENTITY_GROUP, un HeaderField tipo group = varias celdas en una fila: Indicador con su lista, prefijo y número del folio) en facturas, boletas, reservas y notas de crédito de ventas y compras (BILLING_DOCUMENT) y en el asiento. Una empresa que los sustituye por campos de usuario arma su grupo con fieldGroup(label, cells) en headerRight de su extensión y oculta el del base con hideHeaderFields: ['Indicator'] (oculta el grupo completo). Las tres fechas de la cabecera de todo documento de ventas y compras van juntas y en este orden, una debajo de la otra: contabilización, vencimiento y del documento (TaxDate), las tres con la fecha de hoy al crear. Pestañas Contenido, Logística y Finanzas en ventas y compras (layout.tabs, tools/logistica.png y tools/finanzas.png): Logística = destino y dirección de facturación (se eligen de las direcciones del socio, con su texto debajo), forma de envío, número de seguimiento y bloquear reclamaciones; Finanzas = entrada en el diario, cuenta asociada, bloqueo de pago, condiciones y forma de pago (de cobro en ventas, de pago en compras), indicador banco central, plazos, recálculo del vencimiento y proyecto. Al elegir el socio se cargan sus contactos y direcciones (BusinessPartners(cod) con BPAddresses y ContactEmployees) y se prellenan las direcciones, condiciones, forma de pago y cuenta asociada que trae (solo si el campo está vacío). La persona de contacto va bajo el nombre del socio. Las transferencias de stock y las solicitudes de traslado también llevan socio, contacto, destino, lista de precios (PriceList) y empleado (SalesPersonCode). Los códigos numéricos de las listas se guardan como número (typedLookup). Las extensiones de cliente pueden agregar campos a las pestañas (logisticsLeft/Right, financeLeft/Right). Sin verificar: «Responsable» de Logística (no se identificó su campo del Service Layer), canal del socio, cuota/consolidación de Finanzas. Solicitudes de compra (/compras/solicitudes, tools/solicitud de compra.png) son documentos de marketing sin socio: marketingLayout({ requester: true }) — cabecera con tipo, solicitante (usuario de SAP: al elegirlo se completan nombre, correo, departamento y sucursal; ReqType 12), «Válido hasta», fecha necesaria e indicador/proyecto; líneas con proveedor sugerido (LineVendor), fecha necesaria por línea (columna date de la grilla) y cantidad/precio/descuento/impuesto/total/almacén; pie con gastos adicionales y totales; sin moneda, redondeo ni pestañas. Para el solicitante la API registra Users con selectOnly (UserCode, UserName, eMail, Department, Branch: $select obligatorio y limitado; la entidad trae contraseña y permisos), más Departments y Branches. Los documentos sin socio indican su maestro de impuestos con DocumentScreen.taxResource. La dirección del documento (AddressExtension: ShipTo… y BillTo…, address-fields.ts): el texto bajo el destino y la dirección de facturación se arma con sus partes (calle, número, bloque, edificio, ciudad, código postal, provincia, región, país) del documento guardado o, si se eligió otra, de la dirección del socio; el botón «…» abre una ventana (address-dialog.ts) para corregirlas en este documento y solo lo que cambió viaja en AddressExtension; elegir otra dirección descarta las correcciones. Las celdas de lista con texto propio para el código (VhLookup.codes: la cuenta asociada con su código con formato) lo muestran también en los campos de cabecera (sbo-lookup-field).
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.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:
- 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: 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 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).
Redondeo (Rounding, RoundingDiffAmount): fila «Redondeo» (casilla + importe) en el pie de ventas y compras, después de «Gastos adicionales» y antes del impuesto (como en SAP; ver tools/factura.png). Los campos siempre existen en el documento (se leen y se conservan); la fila solo se ve si la empresa usa redondeo por moneda (OADM.RoundMthd, RoundingMethod = tYES en el Service Layer; confirmado con el usuario; por documento no se muestra). Esa bandera y el separador de segmentos de las cuentas salen de GET /api/company/settings (backend: CompanyService, lee CompanyService_GetAdminInfo y entrega solo esos dos campos, con caché de 10 min). El importe solo cuenta con la casilla marcada; sin marcarla no se envía nada y SAP calcula el suyo. Facturas y notas de crédito (billing: true, BILLING_DOCUMENT en screen-kit.ts): el pie suma «Anticipo total» (DownPaymentAmount, tras el descuento), «Importe aplicado» (PaidToDate) y «Saldo pendiente» (DocTotal − PaidToDate, 0 en un documento nuevo), de solo lectura, como en SAP. Gastos adicionales hechos: tabla «Gastos adicionales» en el pie de ventas y compras (una fila por gasto: código con su ventana de selección sobre AdditionalExpenses, nombre, importe, impuesto y comentarios; colección DocumentAdditionalExpenses). Se suman al total con su propio impuesto (computeTotals); al modificar, si cambian las filas se envía la colección completa con B1S-ReplaceCollectionsOnPatch (quitarlas todas envía []); expenses: false en marketing(...) la quita de un documento; las extensiones pueden ocultarla con el nombre DocumentAdditionalExpenses. Falta: botón PDF. En ventas un gasto adicional sin cuenta de ingresos falla en SBODemoCL ("Define the freight revenue account…"; 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)
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.