Integración con Microsoft Dynamics NAV 2009 R2
1. Alcance real de la integración
NAV 2009 R2 genera el albarán, Cargoffer lo convierte en el documento de control (DeCA) con QR y devuelve un PDF para archivar o imprimir. La integración es saliente: NAV (o un agente en el mismo servidor) llama a la API REST de Cargoffer. No hay que abrir puertos de entrada ni exponer NAV.
Lo que la API sí ofrece hoy
- OCR del albarán en PDF y prefill con los datos del transporte (
POST /deca/ocr/analyze+GET /deca/ocr/status/{job_id}). - Alta del DeCA con el
service_codeque tú decidas (útil para idempotencia) y adjunto del albarán original (POST /deca/crear,POST /deca/documento/upload/{service_code}). - URL pública y token del QR (
POST /deca/{service_code}/qr) y descarga del QR en PNG (GET /deca/{service_code}/qr.png). - PDF del DeCA generado por Cargoffer, que ya incluye el QR impreso (
GET /deca/{service_code}/pdf).
/deca/{service_code}/pdf es el PDF del DeCA, no tu albarán reestampado. Si el requisito es imprimir el QR sobre el albarán propio, hay que componerlo en el informe de NAV usando el PNG del QR (sección 6); el estampado automático del PDF original está pendiente como desarrollo de producto y este tutorial no lo presenta como disponible.
Requisitos
- NAV 2009 R2 (clásico, C/AL) con base de datos SQL Server (para NAS se usa
nassql.exe). - Cuenta de empresa en Cargoffer con API Key y saldo de créditos: el análisis OCR y el alta del DeCA consumen monedero y pueden devolver
402 SALDO_INSUFICIENTE. - Conectividad HTTPS saliente desde el servidor NAV hacia
https://ecmr.api.pro.cargoffer.com(producción) ohttps://ecmr.api.release.cargoffer.com(pruebas). - Servidor con TLS 1.2 disponible (Windows Server 2008 R2 o superior actualizado).
2. Dos arquitecturas posibles
| Camino | Cómo funciona | Cuándo usarlo |
|---|---|---|
| A. Agente local (recomendado) | NAV deja el PDF en una carpeta vigilada (o en una tabla de cola). Un servicio Windows lee el fichero, llama a la API y escribe el resultado (.json + .pdf) en la carpeta de retorno. NAV recoge el resultado con su cola de trabajos. |
Equipos con poca capacidad de modificar C/AL, o cuando se quiere aislar TLS, multipart y reintentos del ERP. |
| B. C/AL directo desde NAS | Una Codeunit ejecutada por nassql.exe construye el multipart y llama a la API con MSXML2.ServerXMLHTTP (Automation). |
Si el integrador de NAV domina Automation y queréis todo dentro del ERP sin servicios extra. |
3. Sacar el albarán en PDF desde NAV
NAV 2009 R2 no exporta informes a PDF de forma nativa: hay que imprimir contra una impresora PDF o generar el fichero con una utilidad. Tres opciones, de más a menos recomendable:
Opción 1 — Impresora PDF con API COM (nombre de fichero controlado)
- Instala en el servidor NAV (o en el servidor de NAS) una impresora PDF con interfaz COM: PDFCreator (objeto
PDFCreator.JobQueue) o Bullzip PDF Printer (objetoBullzip.PDFWriter). - Marca esa impresora como predeterminada del usuario que ejecuta el proceso (o en la selección de impresora del informe) y desactiva los diálogos de guardado.
- Desde la Codeunit: imprime el informe del albarán y, a continuación, espera el trabajo en la cola de la impresora y guárdalo con la ruta que tú decidas (
ALB-2026-00123.pdf). El nombre controlado es lo que permite casar después el fichero con el registro de NAV.
// C/AL — imprimir el albarán y esperar el PDF en la cola CurrReport.SHOWOUTPUT := FALSE; REPORT.RUN(REPORT::"Albaran", TRUE, FALSE, AlbaranRec); // La impresora PDF ya está seleccionada para ese informe y usuario. // Después, con PDFCreator.JobQueue (Automation): Cola.Initialize; Cola.WaitForJob(30000); Job := Cola.NextJob; Job.SetProfileByNames('Cargoffer'); Job.ConvertTo(RutaPdf); Cola.ReleaseCom;
Si preferís no tocar la selección de impresora por usuario, generad el PDF con un add-on de informes (PrintIt, etc.) que acepte ruta de salida como parámetro. Lo importante es que el fichero resultante tenga un nombre determinista derivado del número de albarán.
Opción 2 — Cola de impresión a carpeta vigilada
La impresora PDF guarda automáticamente en C:\CARGOFFER\ENTRADA con nombre generado por el driver. Un agente detecta el fichero nuevo, lo identifica (por el número de albarán impreso en el propio PDF, o por un registro de cola que NAV escribe antes de imprimir) y lo renombra. Es la opción menos invasiva para C/AL, pero exige disciplina en la carpeta y control de duplicados.
Opción 3 — NAS y cola de trabajos propia
Nas.exe/Nassql.exe arranca una función de la Codeunit 1 (trigger 99, parámetro de arranque startupparameter) y desde ahí se procesa una tabla de cola propia («Envíos DeCA»). Se instala como servicio:
nassql.exe appservername=NAV-CARGOFFER, nettype=TCP, servername=SQL01, database="NAV Produccion", company="Amenabar", startupparameter=DECA, objectcache=40000, installasservice
4. Autenticación con API Key
Para procesos desatendidos no se usa JWT de usuario: se usa una API Key de empresa. Se genera una sola vez (la clave completa solo se muestra en la creación, después queda enmascarada) y se envía en la cabecera x-api-key. La API también acepta ?apikey= en query y Authorization sin Bearer, pero para NAV usad siempre la cabecera: las URLs acaban en logs.
x-api-key: <API_KEY> Accept: application/json
temp_code de la clave es lo que permite identificarla sin exponerla.
5. Paso 1 — Analizar el albarán (OCR)
El análisis es asíncrono: la subida devuelve un job_id y el resultado se consulta por polling. Está diseñado así porque un OCR tarda más que un timeout razonable de HTTP.
5.1 Subir el PDF
POST /deca/ocr/analyze 📖 x-api-key: <API_KEY> Content-Type: multipart/form-data; boundary=----CFR ------CFR Content-Disposition: form-data; name="document_type" delivery_note ------CFR Content-Disposition: form-data; name="file"; filename="ALB-2026-00123.pdf" Content-Type: application/pdf <bytes del PDF> ------CFR--
Respuesta 200:
{
"job_id": "ocr-abc123def456",
"status": "processing",
"document_type": "delivery_note",
"original_url": "/deca/ocr/original/ocr-abc123def456"
}
Valores admitidos de document_type: dct, cmr, delivery_note (albarán), transport_waybill, customs_dua, invoice, packing_list, generic. Errores: 400 FILE_REQUIRED, 401 NO_TOKEN_OR_APIKEY, 502 OCR_SERVICE_ERROR.
5.2 Consultar el resultado
GET /deca/ocr/status/{job_id}?document_type=delivery_note 📖 x-api-key: <API_KEY>
Mientras procesa devuelve status: "processing" (y el array stages con el avance). Cuando termina:
{
"job_id": "ocr-abc123def456",
"status": "completed",
"prefill": {
"service_code": "ALB-2026-00123",
"viaje": {
"cargo_description": "PALETS DE PESCADO CONGELADO",
"cargo_weight": 12450,
"pieces": 22,
"etl_address": "MUELLE 3, PUERTO DE PASAIA, GIPUZKOA",
"etd_address": "POL. IND. JUNDIZ 12, VITORIA-GASTEIZ",
"matricula_tractor": "1234ABC",
"driver_name": "NOMBRE CONDUCTOR"
}
},
"confianza": {
"viaje.driver_name": 0.93,
"viaje.cargo_description": 0.88,
"viaje.cargo_weight": 0.97
},
"document_type": "delivery_note",
"original_url": "/deca/ocr/original/ocr-abc123def456"
}
Si el job falla, responde 502 OCR_ANALYSIS_ERROR. Recomendación: polling cada 3–5 s con un máximo de 20 intentos (60–100 s) antes de marcar el albarán como error recuperable.
document_type del status acepta el mismo enum que el analyze (dct, cmr, delivery_note, transport_waybill, customs_dua, invoice, packing_list, generic). delivery_note es el que hace que el prefill venga mapeado como albarán; si lo omitís, el backend mapea como cmr.
6. Paso 2 — Crear el DeCA y adjuntar el albarán
El alta usa el service_code como clave de idempotencia: el número de albarán normalizado. Debe cumplir ^[A-Z0-9][A-Z0-9-]{2,63}$ — mayúsculas, sin espacios, sin barras ni puntos. Ejemplo válido: ALB-2026-00123.
POST /deca/crear 📖 x-api-key: <API_KEY> Content-Type: application/json { "service_code": "ALB-2026-00123", "sistema": "ambos", "rol_titular": "cargador", "viaje": { "etl_date": "2026-11-10T07:30:00+01:00", "etd_date": "2026-11-10T16:00:00+01:00", "etl_address": "MUELLE 3, PUERTO DE PASAIA, GIPUZKOA", "etd_address": "POL. IND. JUNDIZ 12, VITORIA-GASTEIZ", "cargo_description": "PALETS DE PESCADO CONGELADO", "cargo_weight": 12450, "pieces": 22, "matricula_tractor": "1234ABC", "driver_name": "NOMBRE CONDUCTOR", "deca_finalidad": "control" } }
Respuesta 200 con el documento creado en data (incluye service_code y estado). Códigos relevantes:
| Código | Significado | Qué hacer |
|---|---|---|
409 SERVICE_CODE_EXISTS | Ese albarán ya tiene DeCA | No es un fallo: consultad GET /deca/{service_code} y continuad con el QR/PDF. |
402 SALDO_INSUFICIENTE | La cuenta no tiene créditos | Parar la cola y avisar a administración; reintentar no sirve. |
404 COMPANY_NOT_FOUND | La API Key no resuelve a una empresa | Revisar la clave y su empresa asociada. |
400 | Validación (fecha, cargo_weight no positivo, campo desconocido en viaje) | Corregir el payload; viaje es estricto y rechaza claves no soportadas. |
etl_address/etd_address aceptan texto libre además de ObjectId. El backend intenta resolverlo contra la agenda y, si no hay coincidencia, conserva el texto para que no se pierda en el PDF. Para NAV es lo cómodo: enviad exactamente la dirección del albarán.
6.1 Adjuntar el albarán original (evidencia)
POST /deca/documento/upload/{service_code} 📖 x-api-key: <API_KEY> Content-Type: multipart/form-data file = ALB-2026-00123.pdf tipo = otro
Tipos admitidos por el modelo: cmr, licencia, seguro, tacografo, adr, permiso_internacional, factura, otro. Si vuestro albarán hace las veces de carta de porte, usad cmr; si es un documento interno, otro.
6.2 Atajo: crear el DeCA desde el job OCR
Existe una llamada que hace el alta y adjunta el original en un solo paso:
POST /deca/crear-con-ocr x-api-key: <API_KEY> { "job_id": "ocr-abc123def456", "document_type": "delivery_note", "sistema": "ambos" }
Crea el DeCA con el prefill del OCR y adjunta el PDF original como documento de tipo cmr. Devuelve deca, prefill, confianza y document_type. Si el job no ha terminado responde 400 OCR_JOB_INCOMPLETED. Limitación: el service_code lo genera Cargoffer, así que si necesitáis vuestro número de albarán como identificador, usad el flujo 6.1 con POST /deca/crear.
7. Paso 3 — QR y PDF del DeCA
7.1 Generar la URL pública del QR
POST /deca/{service_code}/qr 📖 x-api-key: <API_KEY> # 200 { "status": 200, "data": { "qr_url": "https://ecmr.cargoffer.com/deca/public/ALB-2026-00123?token=...", "token": "..." } }
El token es estable: llamar de nuevo no lo rota. Guardad qr_url y token en la tabla de NAV y no volváis a pedirlos en cada impresión.
7.2 Descargar el QR en PNG (para imprimirlo en tu albarán)
GET /deca/{service_code}/qr.png?token={token} # Content-Type: image/png · 320 px · corrección M # Con ?format=json devuelve { qr_url, qr_png_data_uri }
Esta ruta es pública y se autoriza con el token (por eso no sirve sin él). Es la pieza que permite al informe de NAV componer el albarán con el QR encima: NAV descarga el PNG una vez y lo inserta en el layout. Errores: 400 TOKEN_REQUIRED, 400 QR_NO_GENERADO, 401 TOKEN_INVALIDO, 404 DECA_NOT_FOUND.
7.3 Descargar el PDF del DeCA
GET /deca/{service_code}/pdf 📖 x-api-key: <API_KEY> # 200 application/pdf · Content-Disposition: attachment; filename="deca-ALB-2026-00123.pdf"
El PDF se genera on-demand con Gotenberg, así que la primera petición puede tardar unos segundos. Errores: 404 DECA_NOT_FOUND, 500 PDF_GENERATION_ERROR, 503 PDF_SERVICE_UNAVAILABLE (servicio de PDF caído → reintentar más tarde, es transitorio).
8. Cómo llamar a la API desde C/AL
En NAV 2009 R2 la vía es Automation (COM). MSXML2.ServerXMLHTTP usa WinHTTP y funciona en el service tier y en NAS; MSXML2.XMLHTTP depende de la sesión del cliente y no sirve para procesos desatendidos.
8.1 Llamada síncrona básica
PROCEDURE LlamarApi@1000(Metodo@1000 : Text; Url@1001 : Text; Cuerpo@1002 : Text;
TipoContenido@1003 : Text; VAR Estado@1004 : Integer;
VAR Respuesta@1005 : Text[1024]) : Boolean
VAR
Http@1100 : Automation "MSXML2.ServerXMLHTTP.6.0";
Error@1101 : Text;
BEGIN
IF ISCLEANAUTOMATION THEN
CREATE(Http, FALSE); // FALSE = service tier / NAS
Http.setTimeouts(10000, 10000, 120000, 300000); // resolver, conectar, enviar, recibir
Http.open(Metodo, Url, FALSE); // FALSE = síncrono
Http.setRequestHeader('x-api-key', ClaveApi);
Http.setRequestHeader('Accept', 'application/json');
IF TipoContenido <> '' THEN
Http.setRequestHeader('Content-Type', TipoContenido);
IF Cuerpo <> '' THEN
Http.send(Cuerpo)
ELSE
Http.send;
Estado := Http.status;
Respuesta := Http.responseText;
EXIT(TRUE);
END;
TRY..CATCH (llegó con NAV 2013). Usad el atributo [TryFunction] en las funciones que llaman a COM/HTTP y comprobad el retorno; así un fallo de red no aborta la cola entera ni deja el registro a medias.
8.2 Multipart y binarios
El análisis OCR exige multipart/form-data con bytes binarios. C/AL no tiene un tipo binario, así que se usa ADODB.Stream (Automation): se construye el preámbulo de texto en un stream, se añade el PDF como binario y se guarda el cuerpo en un fichero temporal que se envía como texto. La secuencia:
- Genera una frontera única (
CFR+ GUID) y el salto de línea comoText[2]conCRLF[1] := 13; CRLF[2] := 10;. - Escribe las cabeceras de cada parte (
--frontera,Content-Disposition,Content-Type) en unADODB.Streamde tipo texto (UTF-8) y guárdalo en un.tmp. - Con dos
ADODB.Streambinarios, concatena: preámbulo + bytes del PDF + cierre--frontera--. Se copia conBytes := Origen.Read(-1); Destino.Write(Bytes);pasando unVariant. - Envía el fichero resultante con
ServerXMLHTTP.send(Cuerpo)tras leerlo como texto (los PDF de NAV son ASCII-safe en cabeceras; si no, enviadlo como binario con un componente propio).
.json de vuelta, y el multipart lo hace el agente con HttpClient.
8.3 Leer las respuestas JSON
NAV 2009 R2 no trae parser JSON (el codeunit 5459 «JSON Management» es de versiones posteriores). Para los cuatro campos que necesitáis (job_id, status, service_code, qr_url/token) basta una extracción por cadena:
PROCEDURE ExtraerCadena@1010(Json@1000 : Text; Clave@1001 : Text) : Text[250]
VAR
Etiqueta@1100 : Text;
Ini@1101 : Integer;
Fin@1102 : Integer;
BEGIN
Etiqueta := '"' + Clave + '":"';
Ini := STRPOS(Json, Etiqueta);
IF Ini = 0 THEN
EXIT('');
Ini := Ini + STRLEN(Etiqueta);
Fin := STRPOS(COPYSTR(Json, Ini), '"');
EXIT(COPYSTR(Json, Ini, Fin - 1));
END;
Text no pasa de 1024 caracteres. El prefill completo del OCR supera ese tamaño: guardad el cuerpo de la respuesta en un fichero con ADODB.Stream (Stream.SaveToFile) y procesadlo ahí, o usad BigText. Si solo necesitáis el service_code y el qr_url, el truncado no os afecta porque van al principio.
9. Errores, reintentos e idempotencia
Reglas de reintento
5xxy503 PDF_SERVICE_UNAVAILABLE/502 OCR_SERVICE_ERROR: transitorios. Reintento con espera exponencial (5 s, 15 s, 60 s), máximo 3 intentos, y el registro vuelve a la cola.401: no reintentar en bucle. Marcar la clave como inválida y avisar; un bucle con clave mala solo llena logs y puede disparar el bloqueo por escaneo.402: parar la cola. Es un problema de saldo, no de red.409: tratarlo como éxito (el DeCA ya existe).400: nunca reintentar sin corregir el payload; guardad el cuerpo de la respuesta en el registro de error.
Idempotencia por albarán
- Tabla propia «Envíos DeCA» con: nº de albarán,
service_code,job_id, estado, intentos, último error, ruta del PDF de entrada, ruta del PDF del DeCA,qr_url,token. - Antes de crear:
GET /deca/{service_code}. Si devuelve200, el DeCA ya existe → saltar al QR/PDF. Si404 DECA_NOT_FOUND, crear. - Si el alta devuelve
409 SERVICE_CODE_EXISTS(carrera entre dos procesos), continuar como si fuera éxito. - Guardar el
job_iden cuanto llegue: si el proceso muere entre subida y creación, se puede retomar la creación con el job existente (/deca/crear-con-ocr) sin volver a pagar un OCR.
409 (que es tratable), pero duplican análisis OCR y consumo.
10. Agente local (opción A) — esqueleto
Servicio Windows o tarea programada que vigila la carpeta de entrada. Requiere .NET Framework 4.5+ / PowerShell 5.1 (en Windows Server 2008 R2 hay que instalar WMF 5.1).
# Vigila C:\CARGOFFER\ENTRADA, llama a la API y deja el resultado en SALIDA $Api = 'https://ecmr.api.pro.cargoffer.com' $Key = $env:CFR_API_KEY # variable de entorno del servicio foreach ($pdf in Get-ChildItem 'C:\CARGOFFER\ENTRADA\*.pdf') { $code = [IO.Path]::GetFileNameWithoutExtension($pdf.Name) # 1) ¿ya existe el DeCA? 404 = hay que crearlo try { Invoke-RestMethod "$Api/deca/$code" -Headers @{ 'x-api-key' = $Key } | Out-Null; $existe = $true } catch { $existe = $false } if (-not $existe) { # 2) OCR: multipart con HttpClient (PS 5.1, .NET 4.5+) Add-Type -AssemblyName System.Net.Http $http = New-Object System.Net.Http.HttpClient $http.DefaultRequestHeaders.Add('x-api-key', $Key) $form = New-Object System.Net.Http.MultipartFormDataContent $form.Add((New-Object System.Net.Http.StringContent('delivery_note')), 'document_type') $bytes = [IO.File]::ReadAllBytes($pdf.FullName) $file = New-Object System.Net.Http.ByteArrayContent($bytes) $file.Headers.ContentType = 'application/pdf' $form.Add($file, 'file', $pdf.Name) $job = ($http.PostAsync("$Api/deca/ocr/analyze", $form).Result.Content.ReadAsStringAsync().Result | ConvertFrom-Json).job_id # 3) polling hasta completed (máx. 20 x 5 s) for ($i = 0; $i -lt 20; $i++) { $st = Invoke-RestMethod "$Api/deca/ocr/status/$job?document_type=delivery_note" -Headers @{ 'x-api-key' = $Key } if ($st.status -eq 'completed') { break } Start-Sleep 5 } # 4) alta con el número de albarán como service_code $body = @{ service_code = $code; sistema = 'ambos'; rol_titular = 'cargador'; viaje = $st.prefill.viaje } | ConvertTo-Json -Depth 8 Invoke-RestMethod "$Api/deca/crear" -Method Post -Headers @{ 'x-api-key' = $Key } -Body $body -ContentType 'application/json' } # 5) QR + PDF de vuelta $qr = Invoke-RestMethod "$Api/deca/$code/qr" -Method Post -Headers @{ 'x-api-key' = $Key } Invoke-RestMethod "$Api/deca/$code/qr.png?token=$($qr.data.token)" -OutFile "C:\CARGOFFER\SALIDA\$code-qr.png" Invoke-RestMethod "$Api/deca/$code/pdf" -Headers @{ 'x-api-key' = $Key } -OutFile "C:\CARGOFFER\SALIDA\deca-$code.pdf" @{ service_code = $code; qr_url = $qr.data.qr_url; estado = 'ok' } | ConvertTo-Json | Set-Content "C:\CARGOFFER\SALIDA\$code.json" }
NAV recoge SALIDA\*.json y el PDF, los registra y los imprime. Este esqueleto no trae reintentos ni bloqueo de fichero: añadid ambos (mover el PDF a PROCESANDO antes de llamar y a ERROR si falla) antes de ponerlo en producción.
11. Checklist de puesta en marcha
- API Key creada y probada con
curl --header "x-api-key: $CFR_API_KEY" https://ecmr.api.pro.cargoffer.com/deca?limit=1desde el servidor NAV (no desde un portátil). - Salida HTTPS correcta desde el servidor: si falla por TLS, revisad protocolos en el registro (WinHTTP usa los del sistema) y no lo resolváis desactivando la validación del certificado.
- Informe de albarán imprimiendo a la carpeta de entrada con nombre determinista y PDF < 4 MB.
- Un albarán de prueba end-to-end: OCR, alta, QR, PDF del DeCA, y verificación de la URL pública abriéndola en el móvil.
- Tratamiento de
409,402y5xxprobado a propósito (por ejemplo, repitiendo el mismo albarán dos veces). - Log sin secretos: ni la API Key ni el token del QR en ficheros de traza.
Endpoints citados
| Método | Endpoint | Uso en la integración |
|---|---|---|
| POST | /deca/ocr/analyze | Subir el albarán en PDF (multipart, document_type) |
| GET | /deca/ocr/status/{job_id} | Polling del OCR y prefill |
| GET | /deca/ocr/original/{job_id} | Recuperar el PDF original analizado |
| POST | /deca/crear | Alta del DeCA con service_code propio |
| POST | /deca/crear-con-ocr | Alta desde el job OCR (adjunta el original) |
| POST | /deca/documento/upload/{service_code} | Adjuntar el albarán como documento de control |
| GET | /deca/{service_code} | Comprobar si el DeCA ya existe (idempotencia) |
| POST | /deca/{service_code}/qr | URL pública y token del QR |
| GET | /deca/{service_code}/qr.png | PNG del QR para el layout del albarán |
| GET | /deca/{service_code}/pdf | PDF del DeCA (con QR impreso) |
| GET | /deca/public/{service_code} | Verificación pública del QR |