Avanzado

Integración con Microsoft Dynamics NAV 2009 R2

Albarán en PDF → OCR → DeCA con QR → PDF de vuelta, sin cambiar el proceso del ERP.

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

Lo que la API no hace (hoy): no existe ningún endpoint que devuelva tu albarán original con el QR sobreimpreso encima. El PDF que devuelve /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

2. Dos arquitecturas posibles

CaminoCómo funcionaCuá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.
Los web services de NAV no se usan aquí. NAV 2009 R2 publica SOAP en el puerto 7047 (formulario 810 «Web Services»), pero Cargoffer expone REST/JSON y multipart, que C/AL no consume de forma nativa. Publicar una Codeunit como web service solo sirve si otro sistema necesita consultar el estado desde NAV, no para esta integración.

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)

  1. 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 (objeto Bullzip.PDFWriter).
  2. 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.
  3. 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
Ojo con el contexto del NAS: el servicio corre con su propia cuenta de Windows, sin sesión interactiva. Esa cuenta necesita (a) permisos de lectura/escritura en las carpetas de entrada y salida, (b) la impresora PDF instalada y visible para ella, y (c) certificados y TLS 1.2 en el equipo. Si algo de esto falla, el PDF sale en blanco o la llamada HTTP muere sin traza útil.

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
Gestión del secreto: guardad la clave en el servicio NAS (cuenta de servicio) o en una variable de entorno del agente, nunca en una tabla de NAV, en el código C/AL ni en el repositorio. Si sospecháis filtración, revocad la clave y generad otra: el 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.

Límite de tamaño: la subida del OCR está limitada a 4 MB por fichero. Un albarán de NAV en PDF suele pesar muy poco, pero si imprimís con imágenes a alta resolución podéis pasarlo: bajad la resolución de las imágenes del informe o enviad el PDF optimizado.

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.

Detalle de contrato: el parámetro 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ódigoSignificadoQué hacer
409 SERVICE_CODE_EXISTSEse albarán ya tiene DeCANo es un fallo: consultad GET /deca/{service_code} y continuad con el QR/PDF.
402 SALDO_INSUFICIENTELa cuenta no tiene créditosParar la cola y avisar a administración; reintentar no sirve.
404 COMPANY_NOT_FOUNDLa API Key no resuelve a una empresaRevisar la clave y su empresa asociada.
400Validación (fecha, cargo_weight no positivo, campo desconocido en viaje)Corregir el payload; viaje es estricto y rechaza claves no soportadas.
Direcciones como texto: los campos 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;
Errores sin TRY/CATCH: C/AL de NAV 2009 no tiene 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:

  1. Genera una frontera única (CFR + GUID) y el salto de línea como Text[2] con CRLF[1] := 13; CRLF[2] := 10;.
  2. Escribe las cabeceras de cada parte (--frontera, Content-Disposition, Content-Type) en un ADODB.Stream de tipo texto (UTF-8) y guárdalo en un .tmp.
  3. Con dos ADODB.Stream binarios, concatena: preámbulo + bytes del PDF + cierre --frontera--. Se copia con Bytes := Origen.Read(-1); Destino.Write(Bytes); pasando un Variant.
  4. 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).
Punto frágil: el multipart en C/AL es la parte que más soporte genera. Si el integrador no lo ha hecho antes, la Opción A (agente local) sale más barata: el ERP solo deja el PDF en una carpeta y lee un .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;
Respuestas largas: en NAV 2009 una variable 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

Idempotencia por albarán

  1. 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.
  2. Antes de crear: GET /deca/{service_code}. Si devuelve 200, el DeCA ya existe → saltar al QR/PDF. Si 404 DECA_NOT_FOUND, crear.
  3. Si el alta devuelve 409 SERVICE_CODE_EXISTS (carrera entre dos procesos), continuar como si fuera éxito.
  4. Guardar el job_id en 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.
Concurrencia: una sola instancia del agente/NAS procesando la cola. Dos workers con el mismo albarán acaban en 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

  1. API Key creada y probada con curl --header "x-api-key: $CFR_API_KEY" https://ecmr.api.pro.cargoffer.com/deca?limit=1 desde el servidor NAV (no desde un portátil).
  2. 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.
  3. Informe de albarán imprimiendo a la carpeta de entrada con nombre determinista y PDF < 4 MB.
  4. 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.
  5. Tratamiento de 409, 402 y 5xx probado a propósito (por ejemplo, repitiendo el mismo albarán dos veces).
  6. Log sin secretos: ni la API Key ni el token del QR en ficheros de traza.

Endpoints citados

MétodoEndpointUso en la integración
POST/deca/ocr/analyzeSubir 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/crearAlta del DeCA con service_code propio
POST/deca/crear-con-ocrAlta 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}/qrURL pública y token del QR
GET/deca/{service_code}/qr.pngPNG del QR para el layout del albarán
GET/deca/{service_code}/pdfPDF del DeCA (con QR impreso)
GET/deca/public/{service_code}Verificación pública del QR
v3.0.0