prestashop-carrier-gls · git:20260930.8a68a16 · 2026-09-30 · sha256 363df2f11373447a
prestashop-carrier-gls git:20260930.8a68a16A
Immutable. This exact content never changes and is served at /api/v1/blob/363df2f11373447a.
---
name: prestashop-carrier-gls
description: API SOAP de GLS España (ASM / asmred) tal y como la usa el módulo oficial glsshipping 3.7.20 de PrestaShop. Endpoints b2b.asmx reales, autenticación por GUID único, las claves exactas de ps_configuration donde el comerciante ya tiene puesto su GUID, la tabla de servicios y horarios, GetExpCli para el seguimiento, etiquetas PDF en base64 y las trampas del código. Usar al integrar GLS/ASM, al leer su GUID desde otro módulo o al consultar el estado de un envío GLS.
---
# GLS España (ASM / asmred) — API real, leída del módulo oficial
**Fuente:** `glsshipping`, versión
**3.7.20** (`config.xml:5`), autor GLS. **Solo lectura: no se modifica ni un byte.**
Es **SOAP 1.2** contra `asmred.com`. El módulo **no usa `SoapClient`**: monta el XML como cadena y
lo manda con cURL (`CURLOPT_POSTFIELDS` + `Content-Type: text/xml; charset=UTF-8`), y parsea la
respuesta con `simplexml_load_string()` + XPath. Eso hace que sea el más fácil de replicar en un
módulo legacy: **no hace falta ninguna extensión más que cURL y SimpleXML**.
---
## 1. Endpoints reales
| Qué hace | URL | Dónde |
|---|---|---|
| **Endpoint que de verdad se usa** | `https://wsclientes.asmred.com/b2b.asmx` | `glsshipping.php:1693`, `AdminGlsshippingController.php:426` |
| Seguimiento (con `?op=`) | `https://wsclientes.asmred.com/b2b.asmx?op=GetExpCli` | `glsshipping.php:2774` |
| URL configurable (valor por defecto) | `https://www.asmred.com/websrvs/ecm.asmx?wsdl` | `glsshipping.php:115`, `:269` |
| Etiqueta por navegador | `https://www.asmred.com/Extranet/public/ecmLabel.aspx?codbarras=<cod>&uid=<GUID>` | `glsshipping.php:1953-1955`, `:2478` |
| Seguimiento público del cliente | `https://mygls.gls-spain.es/e/<codigoBarras>/<cpDestino>/<isoIdioma>` | `glsshipping.php:2039` |
> **Trampa gorda:** la clave `GLS_URL` guarda `https://www.asmred.com/websrvs/ecm.asmx?wsdl`, pero
> el código la **ignora** y la pisa a pelo justo después de leerla:
> ```php
> $URL = Configuration::get('GLS_URL', null, null, $id_shop); // :1687
> $URL = str_replace("http://", "https://", $URL); // :1690
> $URL = 'https://wsclientes.asmred.com/b2b.asmx'; // :1693 ← siempre
> ```
> Lo mismo en el seguimiento (`:2774`) y en `collect()` (`AdminGlsshippingController.php:426`).
> **Cambiar `GLS_URL` no cambia nada.** Un módulo nuevo debe apuntar a `wsclientes.asmred.com`.
**NO CONFIRMADO: no hay entorno de pruebas.** No aparece ninguna URL de PRE en el módulo. GLS
distingue pruebas de producción **por el GUID**: el que trae de fábrica
(`15F9A8B5-82AC-4094-99F7-9FD58FD43E9E`, `glsshipping.php:114`) es el GUID de pruebas, según su
propia descripción en pantalla (`:263`): *«El GUID por defecto […] es para hacer pruebas. […]
solicite su GUID a su Agencia GLS.»*
### Operaciones confirmadas
Todas en el espacio de nombres **`http://www.asmred.com/`**:
| Operación | Para qué | Dónde |
|---|---|---|
| `GrabaServicios` | Dar de alta la expedición y recibir el código de barras y la etiqueta | `glsshipping.php:1710` |
| `GetExpCli` | Consultar el estado de una expedición | `glsshipping.php:2788-2793` |
| `EnviaCorreoAgencia` | Avisar a la agencia de la recogida (cierre del día) | `AdminGlsshippingController.php:432-436` |
---
## 2. Autenticación
**Un solo dato: el GUID.** No hay usuario, contraseña ni token.
- En `GrabaServicios` viaja como atributo del nodo raíz:
`<Servicios uidcliente="<GUID>">` (`glsshipping.php:1713`).
- En `GetExpCli` y en `EnviaCorreoAgencia` viaja como elemento:
`<uid><GUID></uid>` (`glsshipping.php:2791-2793`, `AdminGlsshippingController.php:434`).
- En la etiqueta por navegador, como parámetro: `&uid=<GUID>` (`glsshipping.php:1955`).
Cabecera HTTP obligatoria: `Content-Type: text/xml; charset=UTF-8` (`glsshipping.php:1812`).
Sin ella, GLS devuelve un fallo SOAP.
---
## 3. Claves EXACTAS de `ps_configuration`
Todas son globales salvo donde se indique (varias se leen con `$id_shop`, así que **hay que pasar
el `id_shop`** — ver §8.6).
### 3.1 La credencial
```
GLS_GUID ← LA credencial. Por defecto '15F9A8B5-82AC-4094-99F7-9FD58FD43E9E' (de PRUEBAS)
GLS_URL https://www.asmred.com/websrvs/ecm.asmx?wsdl (se guarda pero NO se usa, ver §1)
GLSSHIPPING_LIVE_MODE
```
Referencias: `glsshipping.php:114-115`, `:262-269`, `:1007-1008`, `:1687-1688`.
### 3.2 Remitente
```
GLS_SENDER_NAME
GLS_SENDER_ADDRESS
GLS_SENDER_CITY
GLS_SENDER_CP
GLS_SENDER_COUNTRY
GLS_EMAIL
```
### 3.3 Opciones del envío
```
GLS_RCS Retorno Copia Sellada (0/1) glsshipping.php:571
GLS_DORIG departamento de origen :590
GLS_VSEC valor asegurado :596
GLS_RETORNO retorno
GLS_COD contrareembolso
GLS_INCOTERM incoterm para EBP fuera de la UE
GLS_SERVICE_DEFAULT
GLS_DEF_PESO peso por defecto
GLS_BULTOS GLS_FIJO_BULTOS GLS_NUM_FIJO_BULTOS GLS_NUM_ARTICULOS
GLS_APLI4 etiquetas múltiples en Apli4 :977
GLS_MULEBP GLS_BPESPT GLS_SDI_COUNTRIES GLS_OPEN_CARRIER
GLS_ENVIAR_MAIL
```
### 3.4 Qué transportistas son de GLS
Cada una guarda **una lista de `id_carrier` separada por `;`** (`glsshipping.php:2928-2952`):
```
GLS_SERVICIO_SELECCIONADO_GLS10
GLS_SERVICIO_SELECCIONADO_GLS14
GLS_SERVICIO_SELECCIONADO_GLS24
GLS_SERVICIO_SELECCIONADO_BUSPAR
GLS_SERVICIO_SELECCIONADO_GLSECO
GLS_SERVICIO_SELECCIONADO_GLSEBP
GLS_SERVICIO_SELECCIONADO_GLSSHOPDELINT
GLS_SERVICIO_SELECCIONADO_GLSPARCEL
GLS_VALID_CARRIERS
```
Se comparan **tanto contra `id_carrier` como contra `id_reference`**, porque PrestaShop crea un
`id_carrier` nuevo cada vez que se edita un transportista.
### 3.5 Estados de pedido y actualización automática
```
GLS_STATUS_SEND estado al grabar el envío
GLS_STATUS_COMPLETED estado al entregar
GLS_STATUS_FAILED estado si no se entrega
GLS_UPDATE_ORDER_STATUS_SEND 1 = aplicar GLS_STATUS_SEND
GLS_UPDATE_ORDER_STATUS_COMPLETED 1 = aplicar GLS_STATUS_COMPLETED
GLS_UPDATE_ORDER_STATUS_FAILED 1 = aplicar GLS_STATUS_FAILED
GLS_DISABLE_STATE_UPDATE
GLS_STATE_UPDATE_DAYS ventana de días hacia atrás que consulta el cron
GLS_STATE_UPDATE_FREQUENCY
GLS_CRON_TIME
GLS_DATE_UPDATE GLS_DATE_CLIENTDATA_UPDATE date_collect
```
### 3.6 Tarifas
```
GLS_CSV_RATES GLS_CSV_RATES_TYPE GLS_SHOW_CSV
```
### 3.7 Tabla propia: `ps_gls_envios`
`glsshipping/sql/install.php:28-46`:
```sql
id_envio, id_envio_order (UNIQUE = id_order), current_state, state_history (JSON),
codigo_envio (varchar 50 = CÓDIGO DE BARRAS), url_track, num_albaran,
codigo_barras (RUTA del PDF en disco, no el código), bultos, retorno, rcs,
peso, vsec, dorig, observaciones, fecha
```
> **`codigo_barras` NO es el código de barras: es la ruta del fichero PDF** en
> `modules/glsshipping/PDF/` (`glsshipping.php:2060-2065`, `:2066`). El código de barras está en
> `codigo_envio`. Nombre desafortunado, pero es así.
---
## 4. Detectar el módulo
```php
if (Module::isInstalled('glsshipping') && Module::isEnabled('glsshipping')) {
$guid = Configuration::get('GLS_GUID');
// ¿está usando todavía el GUID de pruebas?
$esPruebas = ($guid === '15F9A8B5-82AC-4094-99F7-9FD58FD43E9E');
}
```
Nombre en `ps_module`: **`glsshipping`** (`config.xml:3`).
Para saber si un `id_carrier` es de GLS hay dos caminos, los dos en el módulo:
1. `ps_carrier.external_module_name = 'glsshipping'` (`glsshipping.php:2757`).
2. El `id_carrier` **o** el `id_reference` dentro de alguna lista `GLS_SERVICIO_SELECCIONADO_*`
(`glsshipping.php:2957-2985`, función `isCarrierGLS()`).
---
## 5. Seguimiento — `GetExpCli`
`glsshipping.php:2763-2903`. Petición:
```xml
<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetExpCli xmlns="http://www.asmred.com/">
<codigo>CODIGO_DE_BARRAS</codigo>
<uid>GUID</uid>
</GetExpCli>
</soap12:Body>
</soap12:Envelope>
```
Se manda por POST a `https://wsclientes.asmred.com/b2b.asmx?op=GetExpCli` con
`Content-Type: text/xml; charset=UTF-8`.
Respuesta — el módulo la lee con XPath registrando el prefijo `asm` para
`http://www.asmred.com/` (`glsshipping.php:2824-2836`):
```
//asm:GetExpCliResponse/asm:GetExpCliResult
//expediciones/exp
//expediciones/exp/codestado ← el estado, un entero
//expediciones/exp/tracking_list/tracking ← histórico
tipo 'ESTADO' | 'INCIDENCIA' | otros (se descartan)
codigo
evento texto
fecha 'd/m/Y H:i:s'
```
El módulo se queda con los **10 últimos** eventos ordenados por fecha y los guarda como JSON en
`ps_gls_envios.state_history` (`:2856-2866`).
### 5.1 Códigos de estado (`codestado`)
**Lo único CONFIRMADO en el código:**
| `codestado` | Qué hace el módulo | Dónde |
|---|---|---|
| **`7`** | **ENTREGADO.** Si `GLS_UPDATE_ORDER_STATUS_COMPLETED = 1`, pone el pedido en `GLS_STATUS_COMPLETED` | `glsshipping.php:2881-2889` |
| cualquier otro | Si `GLS_UPDATE_ORDER_STATUS_FAILED = 1`, pone el pedido en `GLS_STATUS_FAILED` | `:2890-2900` |
| **`-10`** | Sin datos: el módulo salta ese envío (`continue`) sin tocar nada | `:2879` |
| **`7, 8, 5, 10, 12, 15, 17`** | **Estados finales.** El cron excluye de la consulta cualquier envío que ya esté en uno de ellos | `:2759` |
La consulta del cron (`glsshipping.php:2750-2761`) es:
```sql
... AND e.fecha BETWEEN (CURDATE() - INTERVAL <GLS_STATE_UPDATE_DAYS> DAY) AND (CURDATE() + INTERVAL 1 DAY)
AND e.codigo_envio != ""
AND (e.current_state IS NULL OR e.current_state NOT IN (7,8,5,10,12,15,17))
AND o.current_state NOT IN (<GLS_STATUS_COMPLETED>)
```
**NO CONFIRMADO: la tabla completa de `codestado` con su significado.** El módulo no la tiene: lo
único que distingue es `7` (entregado), `-10` (sin datos) y el conjunto de estados finales. El
resto los mete todos en el mismo saco de «fallido», que es una simplificación del propio módulo,
no de GLS.
*De la documentación pública (fuera del código, [omatech/gls-tracker](https://github.com/omatech/gls-tracker)):*
en el `tracking_list`, una entrada con `tipo = ESTADO` y `codigo = 5` marca una expedición
**anulada**; y los estados que devuelve el servicio se describen como `GRABADO`, `EN REPARTO` y
`ENTREGADO`. **Esto viene de la web, no del código del módulo**, y no cubre los demás `codestado`.
---
## 6. Etiquetas
No hay una llamada aparte: **la etiqueta viene dentro de la respuesta de `GrabaServicios`**. Se
pide con este nodo en la petición (`glsshipping.php:1791-1793`):
```xml
<DevuelveAdicionales>
<Etiqueta tipo="PDF"></Etiqueta>
</DevuelveAdicionales>
```
Y se recoge así (`glsshipping.php:1874-1875`):
```
//Servicios/Envio/Etiquetas/Etiqueta ← PDF en BASE64
//Servicios/Envio/@codbarras ← código de barras (atributo)
//Servicios/Envio/Resultado/@return ← 0 = OK, distinto de 0 = ERROR
//Servicios/Envio/Errores/Error ← texto del error
//Servicios/Envio/Referencias/Referencia[@tipo="N"] ← número de albarán
```
El PDF se descodifica con `base64_decode()` y se escribe en `modules/glsshipping/PDF/`
(`glsshipping.php:2055-2065`). La carpeta tiene que existir y ser escribible; si no, el módulo
imprime los permisos en pantalla (`:1885-1893`).
También se puede abrir la etiqueta ya grabada en el navegador:
```
https://www.asmred.com/Extranet/public/ecmLabel.aspx?codbarras=<codigo_envio>&uid=<GLS_GUID>
```
---
## 7. Tabla de servicios y horarios
`glsshipping.php:1367-1396` y `:1404-1445`. Cada servicio comercial es un par
`<Servicio>` + `<Horario>`:
| Nombre en el módulo | `<Servicio>` | `<Horario>` |
|---|---|---|
| `GLS10` | `1` | `0` |
| `GLS14` | `1` | `2` |
| `GLS24` | `1` | `3` |
| `BUSPAR` (BusinessParcel) | `96` | `18` |
| `ECONOMY` | `37` | `18` |
| `EUROBUSINESSPARCEL` (EBP) | `74` | `3` |
| `SHOPDELINT` | `74` | `19` |
| `PARCELS` (ParcelShop) | `1` | `19` |
**Regla especial:** si el servicio es `74` (EBP) y el peso es **menor de 3 kg**, el módulo cambia
el servicio a **`76`** (`glsshipping.php:1704`):
```php
if ($servicio == 74 && $gls_peso_origen < 3) $servicio = 76;
```
Con `74` o `76` e incoterm mayor que 0, la petición lleva `<Aduanas><Incoterm>` en lugar de
`<Retorno>` (`:1725-1734`).
Códigos de país: **numéricos** para el remitente (`351` para Portugal, `glsshipping.php:1680`) e
**ISO alfabético** para el destinatario (`<Pais>' . $gls_pais . '</Pais>` con `iso_code`, `:1755`).
No son el mismo formato en los dos extremos.
---
## 8. Trampas vistas en este código
1. **`GLS_URL` no se usa.** Ver §1. Cambiar la configuración no cambia el destino.
2. **HTTP 200 con error dentro.** Hay que mirar `Servicios/Envio/Resultado/@return`; si no es `0`,
la expedición NO se ha grabado (`glsshipping.php:1836-1841`).
3. **El módulo guarda los errores en `$_SESSION["ultimoErrorGLS"]`** (`:1834-1840`). En una
petición AJAX o en un cron eso no existe y se pierde. En un módulo nuevo, el error va al
registro del módulo.
4. **`codigo_barras` de `ps_gls_envios` es una RUTA de fichero**, no el código (§3.7).
5. **`GetExpCli` se llama con `curl_multi`** para varios pedidos a la vez (`:2765`, `:2806`). Si se
copia, hay que cerrar los manejadores: el módulo lo hace en `:2822-2823`.
6. **Multitienda a medias.** `grabarServicios()` lee con `$id_shop`
(`Configuration::get('GLS_URL', null, null, $id_shop)`, `:1687-1688`), pero el cron de estados
lee **sin** `id_shop`: `Configuration::get('GLS_GUID')` (`:2763`) y
`Configuration::get('GLS_UPDATE_ORDER_STATUS_COMPLETED')` (`:2881`). En multitienda, el cron usa
el GUID de la tienda del contexto para envíos de todas.
7. **XML montado por concatenación de cadenas.** El módulo mete los textos libres en `<![CDATA[…]]>`
(nombre, dirección, observaciones), pero **los números y los códigos no van escapados**. En
código nuevo, `htmlspecialchars()` en todo lo que no sea CDATA.
8. **TLS sin verificar**: `CURLOPT_SSL_VERIFYPEER => false` en las tres llamadas
(`:1804`, `:2771`, `AdminGlsshippingController.php:440`).
9. **`CURLOPT_CONNECTTIMEOUT => 5` y sin `CURLOPT_TIMEOUT`.** Si asmred acepta la conexión y luego
se queda pensando, la petición se puede eternizar. En código nuevo, poner `CURLOPT_TIMEOUT`.
10. **La recogida (`EnviaCorreoAgencia`) no comprueba la respuesta.** Solo la escribe en el
registro (`AdminGlsshippingController.php:450-453`). Si falla, nadie se entera.
11. **El teléfono y el móvil se rellenan el uno con el otro** si falta alguno
(`glsshipping.php:1699-1700`). Si faltan los dos, viajan vacíos y GLS lo rechaza.
---
## 9. Fragmento PHP listo para copiar (estructura legacy, sin Composer, cURL nativo)
```php
<?php
/**
* Seguimiento GLS/ASM reutilizando el GUID del modulo oficial 'glsshipping'.
* Sin Composer, sin namespace, cURL nativo + SimpleXML.
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class MiGlsTracking
{
const ENDPOINT = 'https://wsclientes.asmred.com/b2b.asmx';
const NS_ASM = 'http://www.asmred.com/';
const NS_SOAP12 = 'http://www.w3.org/2003/05/soap-envelope';
/** Estados que GLS da por finales: no hace falta volver a consultar. */
const ESTADOS_FINALES = array(7, 8, 5, 10, 12, 15, 17);
const ESTADO_ENTREGADO = 7;
const ESTADO_SIN_DATOS = -10;
/** GUID del comercio, tal como lo guarda el modulo oficial. */
public static function getGuid($idShop = null)
{
if ($idShop === null) {
$idShop = (int) Context::getContext()->shop->id;
}
$guid = Configuration::get('GLS_GUID', null, null, $idShop);
return $guid ? (string) $guid : '';
}
/** Codigo de barras (numero de seguimiento) de un pedido. */
public static function getCodigoEnvio($idOrder)
{
$codigo = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT `codigo_envio` FROM `' . _DB_PREFIX_ . 'gls_envios`
WHERE `id_envio_order` = ' . (int) $idOrder
);
return $codigo ? (string) $codigo : '';
}
/**
* Consulta GetExpCli.
*
* @return array|false array('codestado' => int, 'eventos' => array)
*/
public static function getEstado($codigoEnvio, $idShop = null)
{
$codigoEnvio = trim((string) $codigoEnvio);
$guid = self::getGuid($idShop);
if ($codigoEnvio === '' || $guid === '') {
return false;
}
$xml = '<?xml version="1.0" encoding="utf-8"?>'
. '<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
. ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
. ' xmlns:soap12="' . self::NS_SOAP12 . '">'
. '<soap12:Body>'
. '<GetExpCli xmlns="' . self::NS_ASM . '">'
. '<codigo>' . htmlspecialchars($codigoEnvio, ENT_XML1, 'UTF-8') . '</codigo>'
. '<uid>' . htmlspecialchars($guid, ENT_XML1, 'UTF-8') . '</uid>'
. '</GetExpCli>'
. '</soap12:Body></soap12:Envelope>';
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => self::ENDPOINT . '?op=GetExpCli',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $xml,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_FORBID_REUSE => true,
CURLOPT_FRESH_CONNECT => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 20, // el modulo oficial NO lo pone: ponlo tu
CURLOPT_HTTPHEADER => array('Content-Type: text/xml; charset=UTF-8'),
));
$respuesta = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error || $respuesta === false || $respuesta === '') {
return false;
}
// HTTP 200 no garantiza nada: hay que mirar el cuerpo.
$doc = @simplexml_load_string($respuesta, null, 0, self::NS_SOAP12);
if (empty($doc)) {
return false;
}
$doc->registerXPathNamespace('asm', self::NS_ASM);
$result = $doc->xpath('//asm:GetExpCliResponse/asm:GetExpCliResult');
if (empty($result[0])) {
return false;
}
$exp = $result[0]->xpath('//expediciones/exp');
if (empty($exp[0])) {
return false;
}
$codestado = $exp[0]->xpath('//expediciones/exp/codestado');
if (empty($codestado[0])) {
return false;
}
$eventos = array();
$lista = $exp[0]->xpath('//expediciones/exp/tracking_list/tracking');
foreach (is_array($lista) ? $lista : array() as $item) {
$tipo = isset($item->tipo[0]) ? (string) $item->tipo[0] : '';
if (!in_array($tipo, array('ESTADO', 'INCIDENCIA'), true)) {
continue;
}
$eventos[] = array(
'date' => isset($item->fecha[0]) ? (string) $item->fecha[0] : '', // d/m/Y H:i:s
'type' => $tipo,
'code' => isset($item->codigo[0]) ? (string) $item->codigo[0] : '',
'text' => isset($item->evento[0]) ? (string) $item->evento[0] : '',
);
}
return array(
'codestado' => (int) $codestado[0],
'eventos' => $eventos,
);
}
public static function estaEntregado($codestado)
{
return (int) $codestado === self::ESTADO_ENTREGADO;
}
public static function esEstadoFinal($codestado)
{
return in_array((int) $codestado, self::ESTADOS_FINALES, true);
}
}
```
Uso:
```php
$codigo = MiGlsTracking::getCodigoEnvio(1234);
$estado = $codigo ? MiGlsTracking::getEstado($codigo) : false;
if ($estado !== false && MiGlsTracking::estaEntregado($estado['codestado'])) {
// entregado
}
```
---
## 10. Qué NO se ha podido confirmar leyendo el código
- **Tabla completa de `codestado`.** Solo hay `7` (entregado), `-10` (sin datos) y la lista de
estados finales `7,8,5,10,12,15,17`. El significado de `8`, `10`, `12`, `15` y `17` **no
aparece**. Lo que sí se sabe por la web (no por el código) es que en `tracking_list` un
`tipo=ESTADO` con `codigo=5` es una expedición anulada.
- **Códigos del `tracking_list`** (`<codigo>` de cada evento): no hay tabla.
- **Entorno de pruebas**: no existe en el módulo; se usa el GUID de pruebas contra producción.
- **Esquema completo de `GrabaServicios`**: el XML se monta en `glsshipping.php:1707-1794`; ahí
está todo, pero no hay un esquema aparte ni documentación de qué campos son obligatorios.
- **Códigos de error de `Errores/Error`**: llega solo el texto.
- **Formato del `codbarras`** (longitud, prefijo): no se valida. Columna `varchar(50)`.
- **Qué operaciones ofrece `ecm.asmx`** (la URL guardada en `GLS_URL`): el módulo nunca la llama.
Sources: [omatech/gls-tracker](https://github.com/omatech/gls-tracker/blob/master/README.md)