Integración Módulo Viajes

Integración Viajes

Web services para la integración con la plataforma de la agencia de viajes: consulta y creación de terceros proveedores, y registro de reservas (tiquetes, hospedajes y conceptos administrativos) contra una solicitud de viaje (travel.business) ya existente en DrivErp.

Requests

Todas las peticiones se realizan bajo el prefijo /travel:

https://<instancia>/travel/...

Importante: estos endpoints usan el mecanismo estándar de rutas type="json" de Odoo y no leen el cuerpo JSON de forma directa, por lo que el cuerpo debe ser el envoltorio JSON-RPC 2.0 completo, con los datos de la petición dentro de la llave params:

curl -X POST 'https://<instancia>/travel/partner-info' \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "id": null,
    "params": {
      "user": "<usuario>",
      "passwd": "<contraseña>",
      "reference": "900123456"
    }
  }'

Enviar los campos en el nivel superior del JSON, sin envolver en params, resulta en parámetros faltantes y la petición falla.

Las rutas tienen habilitado CORS (Access-Control-Allow-Origin: *) y CSRF deshabilitado, pensado para que la plataforma de la agencia de viajes pueda invocarlas directamente desde su propia aplicación web sin pasar por un backend intermedio.

Vea la guía Autenticación para las credenciales requeridas y Estructura de las respuestas para el formato de la respuesta, que varía por endpoint.

Autenticación

Esta integración no usa token de sesión: las credenciales se envían en params en cada petición, en los campos user y passwd.

  • Las credenciales son un par fijo por entorno, definido en el código del módulo (controllers/utils.py) — no se configuran desde Ajustes ni varían por compañía.
  • passwd se compara en texto plano, sin ningún hash.
  • El mismo par de credenciales aplica a los tres endpoints (/partner-info, /supplier-create, /reserve).
curl -X POST 'https://<instancia>/travel/partner-info' \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "id": null,
    "params": {
      "user": "<usuario>",
      "passwd": "<contraseña>",
      "reference": "900123456"
    }
  }'

Si user/passwd no coinciden, la petición no llega a construir la respuesta habitual del endpoint: result llega directamente como el texto de la excepción HTTP, con el estado HTTP de la respuesta en 200 (ver guía Estructura de las respuestas):

{
  "jsonrpc": "2.0",
  "id": null,
  "result": "401 Unauthorized: The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required."
}

Solicite a su consultor funcional o técnico de DrivErp el usuario y la contraseña habilitados para su instancia.

Estructura de las respuestas

Toda petición devuelve el envoltorio JSON-RPC estándar (jsonrpc, id, result) con estado HTTP 200. La forma de result no es uniforme: cada endpoint tiene su propio formato de éxito, y las validaciones básicas (credenciales, campos vacíos o con palabras no permitidas) devuelven directamente el texto de la excepción en lugar de un objeto — ver guía Autenticación.

/partner-info

Es el único endpoint que no usa el patrón status_code/novelty. Si el tercero existe, result es directamente el objeto con sus datos:

{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "partner_ref": "900123456",
    "partner_name": "Proveedor de ejemplo S.A.S"
  }
}

Si no existe, result es el texto de un error 404:

{
  "jsonrpc": "2.0",
  "id": null,
  "result": "404 Not Found: partner 900123456 not found"
}

/supplier-create y /reserve

Estos sí siguen el patrón {status_code, novelty, ...}: aquí solo existen los estados "00" (éxito) y "99" (rechazado), no existe un estado intermedio de "éxito con novedad".

{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "supplier_ref": "900123456",
    "supplier_name": "Proveedor de ejemplo S.A.S",
    "status_code": "00",
    "novelty": {}
  }
}

En /supplier-create, algunas validaciones son independientes entre sí y pueden acumularse en un mismo novelty (por ejemplo, tercero ya existente junto con ciudad no encontrada):

{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "supplier_ref": "900123456",
    "supplier_name": "Proveedor de ejemplo S.A.S",
    "status_code": "99",
    "novelty": {
      "E04": "error on creation supplier 900123456 already on ERP",
      "N05": "city code not found res.country.city()"
    }
  }
}

Nótese el texto de N05 en el ejemplo: por una particularidad del código, al construir ese mensaje la variable ya fue reemplazada por el resultado (vacío) de la búsqueda de la ciudad, así que el mensaje real no repite el código de ciudad enviado — muestra la representación de un recordset vacío en su lugar.

Códigos de novedad

Los códigos de novedad se agrupan por prefijo (fuente: controllers/utils.py, NOVELTY_CODES):

Código Descripción
N01 Solicitud de viaje (travel) no encontrada
N02 Concepto (travel.concept) no encontrado
N03 Tercero proveedor no encontrado (por VAT)
N04 Tercero pasajero no encontrado (por VAT)
N05 Código de ciudad DANE no encontrado
N06 Código de aeropuerto no encontrado
N07 Centro de costo/analítica no encontrado
V01 La solicitud de viaje no está disponible para procesar reservas
V02 El responsable del viaje (travel_user_ref) no coincide con el de la solicitud
V03 Tipo de reserva (reservation_type) no válido
V04 Código de moneda no válido (solo se admite COP)
V05 Tipo de alojamiento (accommodation_type) no válido
V06 Porcentaje de impuesto no válido (solo 0, 5 o 19)
C01 Más de una coincidencia para el criterio de búsqueda (código reservado en /reserve: para el campo travel la propia búsqueda de estado falla antes de llegar a reportarlo; solo es alcanzable para analytic_code)
E01 / E02 No se permiten valores negativos (impuesto / subtotal-total)
E03 El total no corresponde al subtotal más el impuesto indicado
E04 Se usa en dos sentidos distintos según el endpoint: en /supplier-create, bloquea la creación antes de intentarla (proveedor ya existente, o los 5 campos de datos vacíos a la vez); en /reserve y como fallback general, es el error genérico que envuelve cualquier excepción no controlada durante el proceso (incluidas las causadas por búsquedas sin límite de coincidencias, ver concept/supplier_ref/passenger_ref en /reserve)
E05 Nombre de línea de reserva repetido dentro de la misma petición
E06 El número de factura ya existe: en una reserva previa de cualquier viaje, o en una factura contable ya emitida para el viaje actual
E07 Error al procesar el archivo (XML/PDF) en base64
P01 Código reservado, prácticamente inalcanzable: el chequeo que lo produce (/reserve) no verifica ningún campo real de la compañía, solo que exista el registro de compañía con id 1, lo cual es casi siempre cierto

Terceros

Consultar tercero

GET /travel/partner-info
Sin autenticación

Busca un tercero existente en DrivErp por identificación (reference) y devuelve su nombre. Es un endpoint de solo lectura — pese a declararse como GET, la petición HTTP real es un POST con el envoltorio JSON-RPC; el método GET es a nivel del framework, no de HTTP.

Es el único endpoint de esta integración que no usa el patrón status_code/novelty: la respuesta exitosa es directamente el objeto con partner_ref y partner_name. Si el tercero no existe, result llega como el texto de un error 404 en lugar de un objeto — vea la guía Estructura de las respuestas para el detalle de ambos casos.

Parámetros

ParámetroTipoDescripción
user requerido string Usuario de integración fijo del entorno. Ver guía Autenticación.
passwd requerido string Contraseña de integración en texto plano. Ver guía Autenticación.
reference requerido string Identificación del tercero (res.partner, campo vat), sin dígito de verificación. Se compara únicamente contra los primeros 15 caracteres enviados.
Request
curl -X POST 'https://<instancia>/travel/partner-info' \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "id": null,
    "params": {
      "user": "<usuario>",
      "passwd": "<contraseña>",
      "reference": "900123456"
    }
  }'
Response
{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "partner_ref": "900123456",
    "partner_name": "Proveedor de ejemplo S.A.S"
  }
}

Crear proveedor

GETPOST /travel/supplier-create
Sin autenticación

Crea un tercero proveedor (res.partner, tipo jurídica, documento NIT) a partir de los datos enviados por la agencia de viajes. Publica un mensaje en el chatter del tercero indicando que fue creado desde esta integración.

Todos los parámetros deben enviarse como llave (omitir alguno por completo hace fallar la petición de forma no controlada, ya que son argumentos posicionales de la función), pero el código no rechaza valores vacíos de name/street/city/phone/supplier_email de forma individual: la validación E04 por "campos faltantes" solo se dispara si los cinco llegan vacíos simultáneamente. Es decir, es posible crear un proveedor con, por ejemplo, street vacío sin que se reporte ninguna novedad por eso.

Si el proveedor ya existe (por reference) también responde E04, pero con un texto distinto y de forma mutuamente excluyente con el E04 de campos vacíos (no pueden coexistir ambos). Lo que sí puede acompañar a cualquiera de los dos es N05, si además el código de ciudad no existe — ese chequeo es independiente y puede sumarse al mismo novelty (vea la guía Estructura de las respuestas). Solo se intenta crear el registro si ninguna de estas novedades se produjo.

Parámetros

ParámetroTipoDescripción
user requerido string Usuario de integración fijo del entorno. Ver guía Autenticación.
passwd requerido string Contraseña de integración en texto plano. Ver guía Autenticación.
reference requerido string Identificación del proveedor a crear (res.partner, campo vat), sin dígito de verificación. E04 si ya existe un tercero con esta identificación.
name requerido string Razón social o nombre del proveedor.
street requerido string Dirección del proveedor.
city requerido string Código DANE de la ciudad (res.country.city, campo code). N05 si no existe.
phone requerido string Teléfono del proveedor.
supplier_email requerido string Correo electrónico del proveedor.
Request
curl -X POST 'https://<instancia>/travel/supplier-create' \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "id": null,
    "params": {
      "user": "<usuario>",
      "passwd": "<contraseña>",
      "reference": "900123456",
      "name": "Proveedor de ejemplo S.A.S",
      "street": "Calle 8 # 10-20",
      "city": "11001",
      "phone": "3001234567",
      "supplier_email": "contacto@proveedor.com"
    }
  }'
Response
{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "supplier_ref": "900123456",
    "supplier_name": "Proveedor de ejemplo S.A.S",
    "status_code": "00",
    "novelty": {}
  }
}

Reservas

Registrar reserva

GETPOST /travel/reserve
Sin autenticación

Registra una reserva (travel.reservation) contra una solicitud de viaje (travel.business) ya existente, con sus líneas de tiquetes, hospedajes o conceptos administrativos. Aquí la primera novedad de negocio que se detecta detiene el proceso — la respuesta contiene como máximo una entrada en novelty, salvo las validaciones de formato de las líneas (base64 de xml_invoice/pdf_invoice inválido, campos faltantes en una línea) que también se resuelven antes de llegar a las novedades de negocio.

El viaje (travel) debe existir y estar en un estado que admita reservas (N01/V01), y travel_user_ref debe coincidir con el responsable configurado en la solicitud (V02). El invoice_number de la agencia funciona como llave de unicidad: si ya está asociado a otra reserva activa (de este viaje o de cualquier otro) la petición se rechaza (E06) — no hay mecanismo de idempotencia que devuelva la reserva existente.

Cada línea de lines se valida de forma independiente, y en este orden real: primero la consistencia entre price_subtotal, tax_percent y price_total (E02E01V06E03), y solo después nombre único dentro de la petición (E05), concepto existente (N02), tipo de reserva válido (V03), proveedor y pasajero existentes por identificación (N03/N04) y moneda COP (V04). Si una misma línea dispara más de una de estas novedades, la que se reporta es la de precios/impuestos, no la que aparece primero en la tabla de parámetros. Los campos accommodation_type/lodging_city solo se validan por existencia cuando reservation_type es lodging (V05/N05); en itinerary, la existencia de los aeropuertos (N06) solo se valida cuando es airline, pero el formato de sus tramos se valida en cualquier línea que lo envíe con contenido.

Efectos no reflejados en la respuesta. La reserva se crea directamente en estado progress (sin pasar por el flujo habitual borrador→pendiente→...) y marca travel_business.reservation_state como done. La moneda de la reserva y de sus líneas siempre queda en COP, independientemente del currency_code enviado (que solo se usa para la validación V04). Ninguno de estos efectos se refleja en la respuesta ni en el novelty.

Parámetros

ParámetroTipoDescripción
user requerido string Usuario de integración fijo del entorno. Ver guía Autenticación.
passwd requerido string Contraseña de integración en texto plano. Ver guía Autenticación.
travel requerido string Nombre de la solicitud de viaje existente (travel.business). N01 si no existe, V01 si su estado no admite reservas (debe estar en borrador, pendiente, aprobada, confirmada o en curso). Si el nombre coincide con más de un registro, la petición falla con un error interno no controlado en vez de una novedad específica (el código intenta C01 pero nunca llega a reportarlo: el chequeo de estado se evalúa primero sobre el conjunto de coincidencias y eso ya falla).
tracking_number requerido string Número de seguimiento de la reserva en la plataforma de la agencia.
travel_user_ref requerido string Código del responsable del viaje (travel.user, campo code). V02 si no coincide con el responsable configurado en la solicitud de viaje.
reservation_date requerido date Fecha de la reserva (YYYY-MM-DD).
invoice_number requerido string Número de la factura de la agencia. Se valida contra dos fuentes distintas, ambas con código E06: (1) cualquier travel.reservation no cancelada de cualquier viaje con este mismo invoice_number, y (2) las facturas contables (account.move) ya generadas para las reservas de este mismo travel, comparando su nombre. No hay mecanismo de idempotencia que devuelva la reserva existente: un invoice_number repetido siempre rechaza la petición.
invoice_date requerido date Fecha de la factura de la agencia (YYYY-MM-DD).
analytic_code requerido string Código del centro de costo/analítica (account.analytic, campo code). N07 si no existe, C01 si hay más de una coincidencia.
description requerido string Descripción general de la reserva/viaje.
xml_invoice string Representación XML de la factura electrónica, codificada en base64. E07 si no decodifica correctamente.
pdf_invoice string Representación PDF de la factura, codificada en base64. E07 si no decodifica correctamente.
lines requerido array Líneas de la reserva (tiquetes, hospedajes o conceptos administrativos). Cada línea debe incluir todas las llaves listadas abajo (aunque su valor pueda ir vacío si no aplica al reservation_type de la línea); si falta alguna llave, la petición se rechaza con un error de formato antes de evaluar las novedades de negocio.
name requerido string Nombre/descripción corta de la línea. Debe ser único entre las líneas de la misma petición (E05).
concept requerido string Código del concepto de gasto (travel.concept, campo code). N02 si no existe. Si el código coincide con más de un concepto, la búsqueda no lo detecta (no valida cardinalidad) y el registro múltiple puede provocar un error interno no controlado más adelante en el proceso, reportado genéricamente como E04.
reservation_type requerido string Tipo de línea. Determina qué campos aplican: lodging usa accommodation_type/lodging_city, airline usa itinerary. Si ninguna línea de la reserva es airline o lodging (todas administrative), la creación del registro de reserva puede fallar internamente, reportado también como E04 genérico.
Valores: airline lodging administrative
supplier_ref requerido string Identificación del tercero proveedor (res.partner, campo vat). N03 si no existe. Mismo riesgo que concept si el VAT coincide con más de un tercero (sin limit=1 ni chequeo de cardinalidad).
currency_code requerido string Código de moneda. V04 si es distinto de 'COP' (única moneda admitida actualmente). El valor enviado solo se usa para esta validación: la reserva creada queda siempre en COP internamente, sin importar qué se envíe aquí.
Valores: COP
passenger_ref requerido string Identificación del tercero pasajero (res.partner, campo vat). N04 si no existe. Mismo riesgo que concept/supplier_ref si el VAT coincide con más de un tercero.
date_start requerido string Fecha/hora de inicio del servicio (YYYY-MM-DD HH:MM:SS).
date_stop requerido string Fecha/hora de fin del servicio (YYYY-MM-DD HH:MM:SS). La llave debe enviarse; puede ir vacía si no aplica.
description requerido string Descripción de la línea.
accommodation_type requerido string Tipo de alojamiento. Solo aplica (y se valida, V05) cuando reservation_type es 'lodging'; en otros tipos se envía vacío.
Valores: individual doble triple quad queen king twin
lodging_city requerido string Código DANE de la ciudad de alojamiento. Solo aplica cuando reservation_type es 'lodging' (N05 si se envía y no existe); en otros tipos se envía vacío.
voucher_number requerido string Número de voucher/comprobante de la reserva.
authorization_number requerido string Número de autorización de la reserva.
price_subtotal requerido number Valor antes de impuestos. No puede ser negativo (E02).
tax_percent requerido number Porcentaje de impuesto de la línea. No puede ser negativo (E01); solo se admiten 0, 5 o 19 (V06).
Valores: 0 5 19
price_total requerido number Valor total de la línea (con impuesto incluido). No puede ser negativo (E02); debe coincidir con price_subtotal * (1 + tax_percent/100) con una tolerancia de 100 COP (E03).
itinerary requerido array Tramos de vuelo. La existencia de los aeropuertos (N06) solo se valida cuando reservation_type es 'airline'; en otros tipos debe enviarse como lista vacía, ya que si se envía con contenido su formato (travel_date/airport_origin/airport_dest) se valida igual, sin importar el reservation_type de la línea.
travel_date requerido string Fecha/hora del tramo (YYYY-MM-DD HH:MM:SS).
airport_origin requerido string Código del aeropuerto de origen (travel.airport, campo code). N06 si no existe.
airport_dest requerido string Código del aeropuerto de destino (travel.airport, campo code). N06 si no existe.
Request
curl -X POST 'https://<instancia>/travel/reserve' \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "id": null,
    "params": {
      "user": "<usuario>",
      "passwd": "<contraseña>",
      "travel": "VJ/23/5960",
      "tracking_number": "TN/23/002",
      "travel_user_ref": "39623079",
      "reservation_date": "2026-04-25",
      "invoice_number": "FE231",
      "invoice_date": "2026-04-29",
      "analytic_code": "CC0101",
      "description": "Viaje capacitación cliente",
      "xml_invoice": "",
      "pdf_invoice": "",
      "lines": [
        {
          "name": "Tiquete Aereo",
          "concept": "PAN",
          "reservation_type": "airline",
          "supplier_ref": "890100577",
          "currency_code": "COP",
          "passenger_ref": "39623079",
          "date_start": "2026-04-28 00:00:00",
          "date_stop": "2026-05-01 00:00:00",
          "description": "Tiquete aereo Bogota-Barranquilla",
          "accommodation_type": "",
          "lodging_city": "",
          "voucher_number": "0678",
          "authorization_number": "XK127",
          "price_subtotal": 500000,
          "tax_percent": 19,
          "price_total": 595000,
          "itinerary": [
            {"travel_date": "2026-04-28 08:00:00", "airport_origin": "BOG", "airport_dest": "BAQ"},
            {"travel_date": "2026-05-01 20:00:00", "airport_origin": "BAQ", "airport_dest": "BOG"}
          ]
        }
      ]
    }
  }'
Response
{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "travel": "VJ/23/5960",
    "status_code": "00",
    "novelty": {}
  }
}