Conectar sistemas modernos basados en PHP con un ERP robusto como SAP Business One es uno de los retos más comunes y demandantes en el desarrollo corporativo. Cuando construimos aplicaciones a la medida —como portales de autoservicio para colaboradores, módulos de recursos humanos o sistemas de punto de venta y control operativo— nos topamos con una encrucijada técnica fundamental: ¿cómo lograr que las interfaces sean ultrarrápidas sin comprometer la integridad transaccional de SAP?
La respuesta más sólida es la arquitectura híbrida: utilizar ODBC directo (sobre SAP HANA o SQL Server) para lectura ultrarrápida, consultas complejas y paginación en DataTables, combinándolo con el Service Layer (REST API) para todas las operaciones de escritura (creación, edición y eliminación).
Sin embargo, al implementar el catálogo de empleados (OHEM), surgen problemas comunes que pueden costar días de depuración:
- La confusión clásica entre el campo interno automático
Codey el número externoExtEmpNo(Número de Empleado Externo). - La falta de validación de unicidad al capturar códigos manuales.
- El borrado accidental de líneas secundarias al actualizar colecciones hijas en Service Layer (como los roles de empleado en
HEM6). - Errores 404 al invocar endpoints mal nombrados como
/Employeesen lugar de/EmployeesInfo.
En esta guía exhaustiva aprenderás a resolver cada uno de estos detalles paso a paso utilizando CodeIgniter 4, integrando un panel administrativo ágil, modular y seguro.
🎯 Objetivos de la Implementación
Antes de revisar las líneas de código, definamos con claridad qué resolvemos con esta arquitectura:
- 🚀 Rendimiento óptimo en frontend: Cargar listados de cientos o miles de empleados con paginación del lado del servidor (Server-Side DataTables) consumiendo datos mediante ODBC sin sobrecargar el Service Layer.
- 🛡️ Integridad transaccional absoluta: Delegar todas las altas, bajas y modificaciones al Service Layer de SAP B1, garantizando que se disparen las validaciones de negocio del ERP.
- 🔢 Diferenciación estricta entre
CodeyExtEmpNo: Dejar el campoCodecomo un identificador interno de solo lectura generado automáticamente por SAP, y permitir la captura manual delExtEmpNo(ExternalEmployeeNumber), validando en tiempo real que no se duplique en la base de datos. - 👥 Gestión limpia de colecciones anidadas (Roles del Empleado): Administrar la relación 1 a N entre el empleado y sus roles (
HEM6/OHTY) sin sobrescribir ni perder el historial al hacer actualizaciones parciales. - 📦 Arquitectura desacoplada en CodeIgniter 4: Mantener controladores limpios, uso de traits para respuestas JSON unificadas y trazabilidad total mediante un modelo de logs para auditoría de usuarios.
🏗️ La Arquitectura Híbrida: ODBC para Lectura, Service Layer para Escritura
Cuando desarrollamos sobre SAP Business One, depender exclusivamente del Service Layer para alimentar tablas de datos interactivas suele provocar cuellos de botella. Service Layer es un servicio REST OData pensado para transacciones seguras; procesar consultas con múltiples JOIN, filtros de búsqueda textuales con comodines % y paginaciones concurrentes para decenas de usuarios puede saturar el worker de Node/Apache del servidor SAP.
┌─────────────────────────────────────────┐
│ CodeIgniter 4 Backend │
└──────────────┬───────────────────┬──────┘
│ │
[Lectura / Server-Side] │ │ [Escritura / REST]
ODBC Directo │ │ Service Layer
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ SAP HANA / MSSQL │ │ SAP Service Layer │
│ (Vistas, Tablas) │ │ (Validaciones de Negocio)│
└──────────────────┘ └─────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ Base de Datos SAP B1 │
└──────────────────────────┘
¿Por qué leer con ODBC?
- Velocidad nativa: Las consultas se ejecutan directamente en el motor de base de datos relacional (HANA o SQL Server), aprovechando índices y vistas optimizadas.
- Flexibilidad SQL: Permite utilizar funciones nativas de agregación, uniones complejas entre
OHEM(empleados),HEM6(roles asignados) yOHTY(catálogo de roles) sin lidiar con la sintaxis restrictiva de OData$expand. - Compatibilidad con DataTables: Resuelve de manera inmediata los parámetros
draw,start,lengthysearch[value].
¿Por qué escribir con Service Layer?
- Reglas de negocio vivas: Ejecuta las validaciones de integridad referencial, campos obligatorios definidos en parametrizaciones de documento y lógica contable.
- Seguridad: Evita escribir directamente mediante
INSERToUPDATEpor SQL a las tablas de SAP, lo cual viola el soporte y la garantía de SAP. - Disparadores de eventos: Permite que add-ons de terceros y flujos de trabajo (workflows) internos de SAP reaccionen a la creación o modificación del colaborador.
🔍 El Dilema en SAP B1: ¿Code o ExtEmpNo?
Uno de los errores más frecuentes al crear la pantalla de captura para empleados es asumir que el campo Code de la tabla OHEM corresponde a la matrícula o clave manual del trabajador.
En la arquitectura de base de datos de SAP Business One:
empID(Employee ID): Es la llave primaria interna autoincremental de tipo entero. Es el identificador único con el que se relacionan documentos como órdenes de fabricación, llamadas de servicio o firmas de autorización.Code: Es un valor de control alfanumérico interno gestionado por el ERP. En muchas localizaciones y versiones, intentar forzar un valor personalizado aquí puede generar inconsistencias o conflictos de duplicidad interna.ExtEmpNo(External Employee Number): Es el campo estándar previsto por SAP para almacenar la matrícula de nómina externa, la clave del reloj checador o el identificador del sistema de recursos humanos externo. En Service Layer se representa bajo la propiedadExternalEmployeeNumber.
⚠️ El Riesgo de Duplicidad
Por defecto, SAP B1 no siempre bloquea la duplicidad de ExtEmpNo a nivel de base de datos con un índice único, dependiendo de la configuración de la sociedad. Si dos trabajadores reciben el mismo número externo, los módulos de importación de nómina, control de asistencia o facturación electrónica pueden colapsar.
Por ello, la validación debe garantizarse en nuestra capa de aplicación en CodeIgniter 4 antes de enviar la petición HTTP al Service Layer.
⚙️ Implementación del Controlador en CodeIgniter 4
Analicemos cómo estructurar el controlador EmployeeSAPController. Este componente orquesta la lectura veloz vía ODBC, la verificación de duplicados y el envío transaccional vía cURL a Service Layer.
1. Validación de Unicidad previa vía ODBC
Para evitar saturar el Service Layer con peticiones que van a rebotar por validaciones o que permitirían duplicados no deseados, ejecutamos una comprobación ligera previa mediante ODBC:
PHP
private function extEmpNoExists($dataConect, string $extEmpNo, int $currentEmpID = 0): bool {
if (empty($extEmpNo)) {
return false;
}
$conn = @odbc_connect(
$dataConect["nameODBC"],
$dataConect["userODBC"],
$dataConect["passwordODBC"]
);
if (!$conn) {
return false;
}
odbc_exec($conn, 'SET SCHEMA "' . $dataConect["companyDB"] . '"');
$extEmpNoEsc = str_replace("'", "''", trim($extEmpNo));
$sql = 'SELECT COUNT(*) AS total FROM OHEM WHERE "ExtEmpNo" = \'' . $extEmpNoEsc . '\'';
// Si estamos editando, excluimos al propio empleado evaluado
if ($currentEmpID > 0) {
$sql .= ' AND "empID" <> ' . (int) $currentEmpID;
}
$stmt = odbc_exec($conn, $sql);
$exists = false;
if ($stmt && odbc_fetch_row($stmt)) {
$exists = ((int) odbc_result($stmt, 1)) > 0;
}
if ($stmt) {
odbc_free_result($stmt);
}
odbc_close($conn);
return $exists;
}
💡 Nota técnica: Observa el uso de
SET SCHEMApara bases de datos SAP HANA. En HANA, cada sociedad de Business One reside en su propio esquema. Si utilizas Microsoft SQL Server, asegúrate de utilizar la nomenclatura de catálogo correspondiente (USE [BaseDeDatos]).
2. Guardado Transaccional con Service Layer
El método save() recibe los datos del formulario, ejecuta las comprobaciones requeridas y construye el payload JSON hacia la entidad /EmployeesInfo:
PHP
// Construcción del payload para SAP Service Layer
$payload = [
'FirstName' => trim($datos['firstName']),
'LastName' => trim($datos['lastName']),
'ExternalEmployeeNumber' => $extEmpNo // Mapeo exacto hacia ExtEmpNo
];
if (isset($datos['middleName'])) {
$payload['MiddleName'] = trim($datos['middleName']);
}
if (isset($datos['Active'])) {
$activeVal = $datos['Active'];
// Service Layer espera 'tYES' o 'tNO' para los flags booleanos tipo BoYesNoEnum
$payload['Active'] = in_array($activeVal, [true, 1, '1', 'Y', 'tYES'], true) ? 'tYES' : 'tNO';
}
if ($empID <= 0) {
// Creación: POST a la colección raíz
$url = $slRoot . "/EmployeesInfo";
$method = 'POST';
} else {
// Actualización: PATCH a la entidad específica
$url = $slRoot . "/EmployeesInfo({$empID})";
$method = 'PATCH';
}
🧩 El Reto de las Colecciones Hijas: Gestión de Roles sin Pérdida de Datos
En SAP Business One, un empleado puede tener múltiples roles asignados (como vendedor, chofer, técnico o almacenista). En la base de datos relacional, esto se almacena en la tabla secundaria HEM6.
En Service Layer, esta relación se modela como una colección hija dentro de EmployeesInfo llamada EmployeeRolesInfoLines.
El Problema del PATCH Estándar
Cuando ejecutas un PATCH sobre /EmployeesInfo(id) enviando un arreglo en EmployeeRolesInfoLines, la API REST de SAP suele aplicar una mezcla (merge) o simplemente agregar elementos nuevos. Si tu intención es eliminar un rol previamente asignado, omitir ese elemento en el arreglo no basta: SAP conservará los que ya estaban en la base de datos a menos que utilices el header de reemplazo completo.
La Solución: Header B1S-ReplaceCollectionsOnPatch
Para que un PATCH reemplace la lista de roles en su totalidad (permitiendo tanto agregar como quitar roles sin dejar basura en HEM6), agregamos la cabecera:
PHP
$patchHeaders = [
"Accept: application/json",
"Content-Type: application/json",
"User-Agent: PHP",
"B1S-CaseInsensitive: true",
"B1S-ReplaceCollectionsOnPatch: true" // ¡Indispensable para eliminar líneas omitidas!
];
El flujo completo para quitar un rol queda así:
- Hacemos un
GETa/EmployeesInfo(id)?$select=EmployeeID,EmployeeRolesInfoLinespara conocer los roles vigentes. - Filtramos el arreglo en PHP mediante
array_filter(), excluyendo elRoleIDque el usuario eliminó en pantalla. - Enviamos un
PATCHcon la colección depurada y la cabeceraB1S-ReplaceCollectionsOnPatch: true. De este modo, SAP sincroniza exactamente las filas resultantes.
💻 El Frontend: Vistas con DataTables, Modales y Select2 Remoto
El frontend combina una tabla con paginación asíncrona, un modal con arrastre dinámico (draggable) y un buscador dinámico de roles con autocompletado en vivo mediante Select2.
Estructura de Campos en el Modal
Para evitar confusiones operativas, organizamos la captura visualmente de manera jerárquica:
- No. Empleado Externo (
ExtEmpNo): Campo editable, con asterisco de obligatoriedad y validación antes del envío. - Código SAP (
Code): Marcado comoreadonlycon fondo atenuado y el texto “Automático por SAP”. Esto educa al usuario de que ese código no debe inventarse manualmente. - Estado (
Active): Selector desplegable simple entre Activo (Sí) e Inactivo (No).
HTML
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="ExtEmpNo">No. Empleado Externo *</label>
<input type="text" class="form-control" name="ExtEmpNo" id="ExtEmpNo" required placeholder="Ej. EMP-1045">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="Code">Código SAP (Auto)</label>
<input type="text" class="form-control bg-light" name="Code" id="Code" readonly placeholder="Automático por SAP">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="Active"><?= lang('employee.fields.active') ?></label>
<select class="form-control" name="Active" id="Active">
<option value="Y"><?= lang('employee.active_yes') ?></option>
<option value="N"><?= lang('employee.active_no') ?></option>
</select>
</div>
</div>
</div>
🛠️ Buenas Prácticas y Puntos Críticos en Producción
Al desplegar este módulo en servidores reales bajo Linux o contenedores Docker, ten en cuenta las siguientes recomendaciones probadas en campo:
1. Manejo de Codificación con utf8ize
Los controladores ODBC en sistemas Linux (unixODBC con FreeTDS o el cliente nativo de SAP HANA) pueden devolver cadenas codificadas en ISO-8859-1 o Windows-1252 si la collation de la base de datos de SAP contiene caracteres especiales (acentos, eñes o diéresis). Utilizar una función recursiva de conversión a UTF-8 antes de serializar hacia JSON evita pantallas en blanco por errores de json_encode():
PHP
private function utf8ize($mixed) {
if (is_array($mixed)) {
foreach ($mixed as $key => $value) {
$mixed[$key] = $this->utf8ize($value);
}
} elseif (is_string($mixed)) {
return mb_convert_encoding($mixed, 'UTF-8', 'UTF-8,ISO-8859-1,Windows-1252');
}
return $mixed;
}
2. Gestión de Sesiones en Service Layer
Abrir una sesión de Service Layer por cada petición HTTP es costoso en tiempo de respuesta (~200ms a 500ms solo en el handshake de autenticación). Lo ideal en entornos de alto tráfico es almacenar el B1SESSION y el ROUTEID en una memoria compartida rápida (como Redis o la caché de CodeIgniter 4) con un tiempo de vida (TTL) de 25 minutos, renovándolo únicamente cuando SAP devuelva un error 401 Unauthorized.
3. El Endpoint de Borrado Correcto
En la documentación oficial de SAP Business One Service Layer, muchos ejemplos de prueba utilizan nombres genéricos. La entidad expuesta para empleados es formalmente EmployeesInfo, no Employees. Invocar DELETE sobre /b1s/v1/Employees(12) arrojará invariablemente un error HTTP 404 de recurso no encontrado. La URL canónica es:
$$\text{DELETE } \rightarrow \text{/b1s/v1/EmployeesInfo}(id)$$
📌 Conclusión Práctica
Al dividir las responsabilidades de tu arquitectura —dejando las lecturas complejas a ODBC y las escrituras críticas a Service Layer— logras lo mejor de dos mundos: interfaces web instantáneas para el usuario y cumplimiento estricto de las reglas del ERP.
El correcto mapeo de ExtEmpNo hacia ExternalEmployeeNumber, junto con la validación de duplicados y el uso de cabeceras de reemplazo como B1S-ReplaceCollectionsOnPatch, convierte un módulo propenso a fallas en un componente corporativo confiable, mantenible y escalable.
🌐 Conéctate con Cesar Systems
¿Tienes dudas sobre integraciones con SAP Business One, desarrollo en CodeIgniter 4 o administración de servidores Linux? Sígueme en mis canales y redes oficiales para más tutoriales, código abierto y guías técnicas:
- 🌍 Sitio Web & Blog: cesarsystems.com.mx
- 🐙 GitHub: github.com/julio101290
- 📺 YouTube: Cesar Systems en YouTube
- 💼 LinkedIn: Julio César Leyva Rodríguez
- 🐦 X / Twitter: @cesarsystems
Deja un comentario