Cuando abordamos integraciones profesionales con SAP Business One, uno de los errores más graves que cometen los desarrolladores principiantes es intentar escribir directamente en las tablas de la base de datos mediante sentencias SQL INSERT o UPDATE.

⚠️️ El Riesgo de Escribir Directamente en la Base de Datos

Escribir directamente en el motor de base de datos (ya sea SAP HANA o Microsoft SQL Server) no solo viola las directivas de soporte oficial de SAP (invalidando las garantías de mantenimiento del partner), sino que pasa por alto:

  • La ejecución de transacciones concurrentes seguras y bloqueos a nivel de aplicación.
  • Los triggers y mecanismos internos de versionado (LogInst, UserSign, UpdateDate, UpdateTime).
  • La validación de integridad referencial cruzada que los UDOs gestionan internamente.

⚡ El Problema de Usar Service Layer para Consultas Masivas

Por otro lado, depender al 100% de Service Layer para consultar listados extensos, paginaciones en DataTables y filtros complejos con múltiples JOINs puede ralentizar considerablemente la experiencia de usuario. Service Layer es un backend basado en OData/REST que agrega una capa intermedia de serialización JSON y procesamiento HTTP. Para cargar un catálogo con miles de registros en un DataTables interactivo, una llamada OData compleja suele demorar cientos de milisegundos más que una consulta nativa en memoria.

💡 La Solución Elegida: Patrón Híbrido CQRS Simplificado

Nuestra solución adopta lo mejor de ambos mundos mediante una separación limpia de responsabilidades:

                  ┌────────────────────────────────────────────────────────┐
                  │                 Interfaz Web (Frontend)                │
                  │        DataTables / Select2 / Bootstrap / jQuery       │
                  └────────────┬──────────────────────────────▲────────────┘
                               │                              │
                    Lecturas / Filtros AJAX            Respuestas JSON
                               │                              │
                               ▼                              │
┌─────────────────────────────────────────────────────────────┴────────────────────────────────┐
│                       Controlador CodeIgniter 4 (SapUserAuthWHController)                   │
├──────────────────────────────────────────────┬───────────────────────────────────────────────┤
│            FLUJO DE LECTURA (READ)           │           FLUJO DE ESCRITURA (WRITE)          │
│                                              │                                               │
│  - Consultas SELECT optimizadas              │  - Validación de negocio y unicidad           │
│  - Limit / Offset para paginación rápida     │  - Conexión vía cURL a REST API               │
│  - Conexión nativa HDBODBC                   │  - Inyección a UDO oficial 'AutCompra'         │
│  - Consulta directa a HANA en memoria        │  - Login con token de sesión B1SESSION        │
└──────────────────────┬───────────────────────┴───────────────────────▲───────────────────────┘
                       │                                               │
                SQL Nativo (ODBC)                               JSON Payload (REST)
                       │                                               │
                       ▼                                               │
        ┌──────────────────────────────┐                ┌──────────────┴───────────────┐
        │   Base de Datos SAP HANA     │                │   SAP B1 Service Layer       │
        │   OWHS, OUSR, @AUTORIZACOMPRA│                │   Motor OData Transaccional  │
        └──────────────────────────────┘                └──────────────┬───────────────┘
                                                                       │
                                                            Escritura / Validación
                                                                       │
                                                                       ▼
                                                        ┌──────────────────────────────┐
                                                        │   Tablas de Usuario (UDO)    │
                                                        │   @AUTORIZACOMPRA            │
                                                        │   @AUTORIZACOMPRADET         │
                                                        └──────────────────────────────┘
  1. Lectura (Read Engine): Se procesa a través de la extensión odbc de PHP utilizando el driver oficial de SAP HANA (HDBODBC). Ejecuta sentencias SELECT directas contra las tablas maestras (OWHS, OUSR, @AUTORIZACOMPRA, @AUTORIZACOMPRADET) entregando tiempos de respuesta inferiores a 50 milisegundos.
  2. Escritura (Write Engine): Se procesa exclusivamente a través del SAP Service Layer. Las peticiones POST (creación), PATCH (modificación) y DELETE (eliminación) se transmiten en formato JSON respetando la convención de colecciones del UDO registrado.

📊 2. Modelado de Datos: Estructura de las Tablas de Usuario (UDO)

Para comprender cómo interactúa el controlador con el Service Layer, analicemos la estructura exacta de las dos tablas que componen nuestro objeto de negocio.

A. Tabla Cabecera: @AUTORIZACOMPRA (Tipo: Documento / Master Data)

Esta tabla define la entidad principal. Cada registro representa un almacén de SAP que ha sido habilitado para el circuito de autorizaciones:

ColumnaTipo de DatoLongitudDescripción / Función
CodeAlfanumérico50Código del Almacén (FK lógica con OWHS."WhsCode"). Ejemplo: VGZ, PLM.
NameAlfanumérico100Nombre descriptivo del almacén. Ejemplo: VIRTUAL GASOLINA MAZATLAN.
DocEntryNuméricoEnteroIdentificador secuencial autonumérico generado por SAP.
CanceledCarácter1Bandera de cancelación (Y/N).
ObjectAlfanumérico20Identificador del UDO registrado en SAP (AutCompra).
UserSignNuméricoEnteroUsuario que dio de alta el registro.
CreateDateFechaDatetimeFecha de creación del registro en el sistema.
UpdateDateFechaDatetimeFecha de la última modificación.

Un registro real en esta tabla luce de la siguiente manera:

SQL

Code: 'VGZ'
Name: 'VIRUTAL GASOLINA MAZATLAN'
DocEntry: 1
Canceled: 'N'
Object: 'AutCompra'
DataSource: 'I'
CreateDate: '2026-01-05 00:00:00'

B. Tabla Detalle: @AUTORIZACOMPRADET (Tipo: Líneas de Documento)

Esta tabla almacena la relación 1 a N de los usuarios asignados a dicho almacén y las banderas específicas de autorización que poseen:

ColumnaTipo de DatoLongitudDescripción / Función
CodeAlfanumérico50Clave foránea que referencia al Code de la cabecera.
LineIdNuméricoEnteroNúmero consecutivo de línea dentro del documento (1, 2, 3…).
ObjectAlfanumérico20Identificador del UDO (AutCompra).
U_USERIDNumérico/Texto32ID interno del usuario en SAP (corresponde a OUSR."USERID").
U_SolCompCarácter1¿Tiene permiso para crear Solicitudes de Compra? (Y/N).
U_PedidoCarácter1¿Tiene permiso para generar Pedidos / Órdenes de Compra? (Y/N).
U_UserNameAlfanumérico100Nombre real completo del colaborador (ej. EDUARDO GRANADOS).
U_FolioUserAlfanumérico50Código nemotécnico o serie del usuario en SAP (USER_CODE, ej. MZGTEPZA).

Un registro real de línea luce así:

SQL

Code: 'VGZ'
LineId: 1
Object: 'AutCompra'
U_USERID: 235
U_SolComp: 'Y'
U_Pedido: 'Y'
U_UserName: 'EDUARDO GRANADOS'
U_FolioUser: 'MZGTEPZA'

⚙️ 3. El Controlador a Fondo: Implementación de SapUserAuthWHController.php

El controlador es la pieza central encargada de orquestar el flujo de datos. Está diseñado bajo el estándar de CodeIgniter 4, aprovechando traits de respuesta JSON y desacoplando las dependencias mediante modelos modulares.

Examinemos detalladamente los aspectos técnicos más sobresalientes de su código.

3.1 Conexión ODBC y Manejo de Esquemas en SAP HANA

Al conectarse a SAP HANA mediante ODBC, no basta con autenticar la sesión; es imperativo apuntar al esquema exacto donde reside la compañía (companyDB).

PHP

private function connectODBC() {
    $dataConect = $this->serviceLayerModel->first();
    if (!$dataConect) {
        throw new \Exception('No se encontró configuración de conexión SAP.');
    }

    $conn = odbc_connect(
        $dataConect['nameODBC'],
        $dataConect['userODBC'],
        $dataConect['passwordODBC']
    );

    if (!$conn) {
        throw new \Exception('Error conexión ODBC: ' . odbc_errormsg());
    }

    // Fijamos el esquema de la base de datos de SAP en HANA
    if (!odbc_exec($conn, 'SET SCHEMA "' . $dataConect['companyDB'] . '"')) {
        throw new \Exception('Error SET SCHEMA: ' . odbc_errormsg($conn));
    }

    return $conn;
}

Nota de Arquitectura: En SAP HANA, todas las tablas y esquemas deben delimitarse con comillas dobles (") si contienen caracteres especiales o prefijos como @ (ejemplo: "@AUTORIZACOMPRA"). Si se omiten las comillas dobles, el analizador léxico de HANA convertirá el identificador a mayúsculas o arrojará un error de sintaxis inmediata.

3.2 Listado Server-Side para DataTables con Subconsultas

Para evitar el problema de las consultas N+1 al calcular cuántos usuarios tiene configurados cada almacén, implementamos una subconsulta correlacionada directamente en la sentencia de extracción:

PHP

$sql = "
    SELECT
        T0.\"Code\",
        T0.\"Name\",
        T0.\"DocEntry\",
        T0.\"CreateDate\",
        (
            SELECT COUNT(1) 
            FROM \"@AUTORIZACOMPRADET\" D 
            WHERE D.\"Code\" = T0.\"Code\"
        ) AS \"UsersCount\"
    FROM \"@AUTORIZACOMPRA\" T0
    {$whereExtra}
    ORDER BY {$orderBy} {$orderDir}
    LIMIT {$length} OFFSET {$start}
";

Esto permite que el DataTable muestre una insignia dinámica con el número exacto de colaboradores autorizados por cada sucursal sin penalizar el rendimiento del servidor.

3.3 Catálogos Predictivos para Select2: Almacenes y Usuarios SAP

Para proporcionar una experiencia de usuario fluida, el módulo cuenta con dos endpoints ligeros que responden a eventos de búsqueda tipo typeahead (Select2 con AJAX):

Catálogo de Almacenes (getWarehousesAjax)

Consulta los almacenes activos de SAP en la tabla maestra OWHS:

SQL

SELECT "WhsCode", "WhsName"
FROM OWHS
WHERE "Locked" = 'N'
  AND ("WhsCode" LIKE '%BUSQUEDA%' OR "WhsName" LIKE '%BUSQUEDA%')
ORDER BY "WhsCode" ASC
LIMIT 40;

Catálogo de Usuarios (getSapUsersAjax)

Consulta la tabla OUSR para obtener el ID numérico (USERID), el código de usuario (USER_CODE) y el nombre completo (U_NAME). Al mapear la respuesta JSON, asignamos estratégicamente:

PHP

$data[] = [
    'id'        => $userId,                          // 235
    'text'      => $userCode . ' - ' . $userName,    // MZGTEPZA - EDUARDO GRANADOS
    'userCode'  => $userCode,                        // MZGTEPZA
    'userName'  => $userName,                        // EDUARDO GRANADOS
    'folioUser' => $userCode                         // Asignación directa para auto-llenado
];

3.4 Persistencia Transaccional mediante Service Layer

El método save() recibe los datos del formulario, incluyendo el arreglo de filas del detalle serializado en JSON. A continuación, realiza los siguientes pasos críticos:

  1. Validación de Unicidad en Creaciones: Si se trata de un nuevo almacén (isNew === 1), consulta vía ODBC que el Code no exista previamente en @AUTORIZACOMPRA.
  2. Autenticación en Service Layer: Invoca al controlador de autenticación para obtener un SessionId válido y genera la cookie requerida:PHP$cookie = "B1SESSION=" . $conexionSap->SessionId . "; ROUTEID=.node1";
  3. Construcción del Payload OData:En el estándar de Service Layer, cuando un UDO posee tablas hijas, la colección dependiente debe llamarse exactamente con el nombre de la tabla sin el carácter @, seguido del sufijo Collection. Por ende, para @AUTORIZACOMPRADET, la clave obligatoria en el JSON es AUTORIZACOMPRADETCollection:

PHP

$payload = [
    'Name'                        => $name,
    'AUTORIZACOMPRADETCollection' => $linesPayload
];

if ($isNew === 1) {
    $payload['Code'] = $code;
    $url             = $slRoot . "/AutCompra";
    $method          = 'POST';
} else {
    // Al actualizar, se apunta a la clave primaria en la URL
    $url    = $slRoot . "/AutCompra('" . rawurlencode($code) . "')";
    $method = 'PATCH';
}
  1. Registro de Auditoría: Toda operación exitosa se documenta en la bitácora del sistema mediante LogModel, guardando el usuario que ejecutó la acción y la fecha.

💻 4. La Vista Interactiva (sapUserAuthWH.php): Experiencia de Usuario sin Fricción

El frontend fue concebido para minimizar los clics y eliminar la posibilidad de introducir datos inconsistentes. Utiliza Bootstrap 4, AdminLTE 3, DataTables, Select2 y SweetAlert2.

4.1 Estructura del Modal Extendido (modal-xl)

La interfaz del formulario de captura se organiza visualmente en dos bloques bien diferenciados:

  1. Card de Cabecera (Almacén):
    • En modo creación, presenta un selector dinámico Select2 que consulta OWHS. Al seleccionar una bodega, el campo descriptivo Nombre del Almacén se autocompleta inmediatamente.
    • En modo edición, el selector se oculta y en su lugar se presenta un campo de texto plano de solo lectura para evitar alteraciones accidentales de la clave primaria (Code).
  2. Card de Detalle (Usuarios y Derechos):
    • Cuenta con una barra superior de captura rápida compuesta por:
      • Selector AJAX de usuarios SAP (#selectNewUser).
      • Switch de Solicitud de Compra (#checkNewSolComp).
      • Switch de Pedido (#checkNewPedido).
      • Campo de Folio (#inputNewFolioUser), que se auto-rellena con el USER_CODE en el instante en que se selecciona un usuario.
      • Botón de inserción directa con icono +.
    • Una tabla dinámica en memoria (#tableAuthDetails), donde cada fila agregada cuenta con switches activos para alterar permisos sobre la marcha, inputs para ajustar el folio y un botón para eliminar la fila.

4.2 Automatización del Folio de Usuario con Select2

El evento JavaScript que conecta la selección del colaborador con la asignación automática del folio opera de la siguiente manera:

JavaScript

$('#selectNewUser').select2({
    dropdownParent: $('#modalAuthWH'),
    placeholder: 'Buscar usuario SAP...',
    allowClear: true,
    ajax: {
        url: baseControllerUrl + '/getSapUsersAjax',
        dataType: 'json',
        delay: 250,
        data: function (params) {
            return { searchTerm: params.term || '' };
        },
        processResults: function (data) {
            return { results: data.data || [] };
        }
    }
}).on('select2:select', function (e) {
    // Al seleccionar el usuario, recuperamos su USER_CODE y lo asignamos al input
    var selectedData = e.params.data;
    $('#inputNewFolioUser').val(selectedData.userCode || '');
}).on('select2:clear', function () {
    // Si se limpia el selector, reseteamos el campo de folio
    $('#inputNewFolioUser').val('');
});

4.3 Validación de Duplicados en Tiempo Real en el DOM

Para prevenir que un usuario sea dado de alta dos veces en el mismo almacén, antes de insertar la fila se recorre el atributo de datos data-userid de la tabla:

JavaScript

var userId = userData.id;
var exists = false;

$('#tbodyAuthDetails tr').each(function () {
    if ($(this).data('userid') == userId) {
        exists = true;
        return false;
    }
});

if (exists) {
    Swal.fire('Atención', 'El usuario seleccionado ya se encuentra en la lista de autorizaciones.', 'warning');
    return;
}

🚦 5. Configuración de Rutas en CodeIgniter 4

Para integrar este controlador dentro de la estructura de enrutamiento de la aplicación y garantizar que esté protegido por el sistema de control de acceso basado en roles (RBAC), definimos las siguientes directivas dentro del archivo de rutas del módulo:

PHP

// =========================================================================
// RUTAS PARA AUTORIZACIÓN DE ALMACENES SAP (@AUTORIZACOMPRA / AutCompra)
// =========================================================================

// Listado principal y endpoint de datos para DataTables
$routes->get('servicelayer/sapuserauthwh',
    'SapUserAuthWHController::index',
    [
        'filter'    => 'permission:SAPUserAuthWH-permission',
        'namespace' => 'julio101290\boilerplateservicelayer\Controllers'
    ]
);

// Consulta de un almacén y sus usuarios para edición (JSON)
$routes->get('servicelayer/sapuserauthwh/getAuthWH/(:segment)',
    'SapUserAuthWHController::getAuthWH/$1',
    ['namespace' => 'julio101290\boilerplateservicelayer\Controllers']
);

// Catálogo AJAX de almacenes activos (OWHS)
$routes->get('servicelayer/sapuserauthwh/getWarehousesAjax',
    'SapUserAuthWHController::getWarehousesAjax',
    ['namespace' => 'julio101290\boilerplateservicelayer\Controllers']
);

// Catálogo AJAX de usuarios activos de SAP (OUSR)
$routes->get('servicelayer/sapuserauthwh/getSapUsersAjax',
    'SapUserAuthWHController::getSapUsersAjax',
    ['namespace' => 'julio101290\boilerplateservicelayer\Controllers']
);

// Guardado transaccional (Creación POST y Actualización PATCH en Service Layer)
$routes->post('servicelayer/sapuserauthwh/save',
    'SapUserAuthWHController::save',
    ['namespace' => 'julio101290\boilerplateservicelayer\Controllers']
);

// Eliminación de la autorización por almacén
$routes->post('servicelayer/sapuserauthwh/delete/(:segment)',
    'SapUserAuthWHController::delete/$1',
    ['namespace' => 'julio101290\boilerplateservicelayer\Controllers']
);

🔍 6. Casos Reales de Resolución de Problemas (Troubleshooting en SAP HANA)

Durante el desarrollo de esta integración nos topamos con comportamientos particulares de SAP HANA y del Service Layer que vale la pena documentar para ahorrar horas de depuración a otros ingenieros de software.

Problema 1: El Error de Sensibilidad a Mayúsculas en HANA (General error;260 invalid column name: LOCKED)

💥 Síntoma:

Al ejecutar la búsqueda de usuarios de SAP desde el frontend, el backend respondía con un error 500 y el siguiente mensaje de ODBC:

JSON

{
    "data": [],
    "error": true,
    "message": "odbc_exec(): SQL error: [SAP AG][LIBODBCHDB SO][HDBODBC] General error;260 invalid column name: LOCKED: line 4 col 24 (at pos 108), SQL state S1000 in SQLExecDirect"
}

🧐 Causa Raíz:

A diferencia de Microsoft SQL Server (que suele configurarse con intercalaciones Case-Insensitive como SQL_Latin1_General_CP1_CI_AS), SAP HANA es estrictamente Case-Sensitive cuando los nombres de columna se envuelven entre comillas dobles.

En el catálogo interno de SAP, el campo que indica si un usuario está bloqueado se llama "Locked" (con la primera letra mayúscula y el resto minúsculas). Al escribir en el query SQL:

SQL

WHERE "LOCKED" = 'N' -- ❌ Error en HANA

HANA busca literalmente una columna en mayúsculas sostenidas, no la encuentra en el diccionario de datos de OUSR y revienta la ejecución.

✅ Solución:

Ajustar la sentencia respetando la convención PascalCase y considerando valores nulos:

SQL

WHERE ("Locked" = 'N' OR "Locked" IS NULL) -- ✔️ Correcto

Problema 2: Intento de Consulta de Columnas Inexistentes (U_FolioUser en OUSR)

💥 Síntoma:

Al intentar precargar el folio directamente desde la consulta de usuarios:

JSON

{
    "data": [],
    "error": true,
    "message": "odbc_exec(): SQL error: [SAP AG][LIBODBCHDB SO][HDBODBC] General error;260 invalid column name: U_FolioUser: line 2 col 57 (at pos 58), SQL state S1000 in SQLExecDirect"
}

🧐 Causa Raíz:

Se asumió inicialmente que OUSR contaba con un campo de usuario personalizado llamado U_FolioUser. Al realizar una introspección de metadatos sobre la estructura física de OUSR:

SQL

SELECT COLUMN_NAME 
FROM TABLE_COLUMNS 
WHERE TABLE_NAME = 'OUSR' AND COLUMN_NAME LIKE 'U_%';

Se constató que los únicos campos de usuario presentes en esa instalación eran:

  • U_GLO_CostCenter
  • U_empID

El valor de ejemplo que se requería almacenar en U_FolioUser dentro de @AUTORIZACOMPRADET (por ejemplo, MZGTEPZA) no era más que el propio código de inicio de sesión de SAP (USER_CODE).

✅ Solución:

Eliminar la columna inexistente del SELECT y asignar directamente USER_CODE como el valor por defecto para el folio del usuario.

Problema 3: Diferencia de Tipos de Datos en U_USERID (Entero vs String)

💥 Síntoma:

Al enviar el payload a Service Layer, la API retornaba un error HTTP 400 Bad Request:

JSON

{
    "error": {
        "code": -1000,
        "message": {
            "lang": "en-us",
            "value": "Property 'U_USERID' of 'AutCompra' is invalid. Expected type is Edm.String"
        }
    }
}

🧐 Causa Raíz:

Al registrar un campo de usuario (UDF) en SAP Business One mediante la herramienta nativa Herramientas -> Herramientas de personalización -> Campos definidos por el usuario, el administrador puede configurarlo como tipo Alfanumérico (Texto) o tipo Numérico (Entero).

Si en SAP se definió como Alfanumérico, el Service Layer espera recibir "235" en formato cadena, no el entero primitivo 235.

✅ Solución:

Garantizar la compatibilidad en el array de PHP convirtiendo el tipo según corresponda:

PHP

'U_USERID' => (string) $userId, // Si el UDF fue creado como Alfanumérico
// O bien:
'U_USERID' => (int) $userId,    // Si el UDF fue creado como Numérico

📈 7. Impacto y Beneficios de Negocio

La implementación de este módulo no representó únicamente una mejora técnica en el stack de software; supuso una transformación tangible en el día a día operativo de la empresa:

┌───────────────────────────────────────┬───────────────────────────────────────┐
│         ANTES (MÉTODO NATIVO)         │       AHORA (MÓDULO WEB CI4 / SL)     │
├───────────────────────────────────────┼───────────────────────────────────────┤
│ ❌ Requiere licencia activa de SAP    │ ✅ Acceso vía navegador web sin       │
│    Business One para cada operador.   │    consumir licencias profesionales.  │
│                                       │                                       │
│ ❌ Captura manual de USERID numérico  │ ✅ Selectores predictivos Select2     │
│    abriendo ventanas auxiliares.      │    con búsqueda por nombre y código.  │
│                                       │                                       │
│ ❌ Transcripción manual propensa a    │ ✅ Autocompletado inmediato de        │
│    errores del código de folio/serie. │    USER_CODE en el campo de folio.    │
│                                       │                                       │
│ ❌ Tiempos de registro de 5 a 10      │ ✅ Configuración completa en menos    │
│    minutos por almacén.               │    de 30 segundos por sucursal.       │
│                                       │                                       │
│ ❌ Sin control visual consolidado     │ ✅ Vista en tabla con contador de     │
│    de cuántos usuarios están activos. │    usuarios asignados en vivo.        │
└───────────────────────────────────────┴───────────────────────────────────────┘
  1. Eficiencia Temporal: Reducción del 90% en el tiempo necesario para dar de alta o ajustar permisos de compras por sucursal.
  2. Cero Errores de Integridad: La combinación de lectura ODBC y validación Service Layer imposibilita la creación de huérfanos o almacenes inexistentes.
  3. Auditoría Clara: Cada cambio queda registrado en la bitácora interna (LogModel), permitiendo saber con exactitud qué usuario web aplicó las modificaciones.

📋 8. Resumen de Buenas Prácticas para Integraciones con SAP B1

Si estás planificando desarrollar extensiones web o móviles que interactúen con SAP Business One, ten presentes estas directrices aprendidas durante el proyecto:

  • 🛡️ Respeta la Regla de Oro: Utiliza ODBC exclusivamente para consultas de lectura (SELECT). Todas las escrituras deben canalizarse a través de Service Layer o DI API.
  • 🔡 Cuida el Case-Sensitivity en SAP HANA: Comprueba siempre el nombre exacto de tablas y columnas tal y como están registradas en el catálogo del sistema.
  • 📦 Respeta la Nomenclatura de UDOs en Service Layer: Para tablas hijas, recuerda siempre la estructura [NombreTablaSinArroba]Collection.
  • ⚡ Optimiza con Paginación Server-Side: Evita cargar tablas completas en memoria del navegador. Implementa LIMIT y OFFSET en el motor de base de datos para manejar miles de registros sin degradar la experiencia de usuario.
  • 🧩 Modulariza tu Código: Encapsula la lógica de autenticación y consumo de APIs en servicios dedicados para que tus controladores permanezcan limpios y mantenibles.

🌐 Conecta con la Comunidad y Sigue el Proyecto

El desarrollo de integraciones para sistemas ERP como SAP Business One, el software libre y la creación de herramientas de productividad en Linux y PHP son temas que comparto de manera constante en mis plataformas y canales.

Si te interesa profundizar en el código, acceder a repositorios, ver videotutoriales detallados o apoyar el desarrollo de nuevos paquetes de código abierto, te invito a seguirme en todas mis redes oficiales:

¡Déjame en los comentarios tus dudas o cuéntame cómo gestionas las autorizaciones de usuario en tus proyectos de SAP Business One! 🚀💬