Operaciones
Flujo Completo del Dispatch

Guía Completa del Ciclo de Despacho

Esta guía cubre el flujo de extremo a extremo para integrar un sistema externo (ERP) con Smartfleet, desde la creación de facturas hasta la finalización de la guía y gestión de devoluciones. Incluye todos los casos posibles, las reglas de negocio del modelo de cantidades y cómo manejar cada escenario correctamente.


Entidades involucradas

Antes de ejecutar cualquier flujo, es importante entender cómo se relacionan las entidades del sistema:

receipt_types
    └── receipts (facturas)
            └── receipt_lines (líneas con cantidades)
                    └── purchase_orders / transfers (recolecciones externas)

dispatches (guías)
    └── dispatch_stops (paradas)
            ├── tipo: hub       → sede de salida/llegada
            ├── tipo: receipt   → entrega de factura (ENT_FAC)
            ├── tipo: pickup    → recolección OC (REC_OC)
            ├── tipo: transfer  → recolección traslado (REC_TTC)
            └── tipo: return    → recolección devolución (REC_DEV)

returns (devoluciones)
    └── return_lines

El modelo de cantidades (receipt_lines)

Cada línea de una factura tiene múltiples campos de cantidad que representan el estado del ítem a lo largo del proceso logístico. Entender este modelo es fundamental para saber cuándo se necesita recolección y cómo registrar entregas.

CampoDescripción
quantityCantidad total del ítem en la factura
quantity_baseCantidad ya confirmada en la sede; no requiere recolección externa
quantity_pickup_requiredCantidad que debe ser recolectada externamente (OC o traslado)
quantity_pickup_collectedCantidad efectivamente recolectada hasta ahora
quantity_preparedCantidad lista y preparada para despacho en bodega
quantity_deliveredCantidad entregada al cliente
quantity_not_deliveredCantidad no entregada en la última guía
quantity_returnedCantidad devuelta por el cliente
quantity_not_collectedCantidad no recolectada (recolección fallida)

Restricción clave:

quantity = quantity_base + quantity_pickup_required

Ejemplos:

Escenarioquantityquantity_basequantity_pickup_required
Todo en sede, sin recolección10100
Todo debe recolectarse10010
Parte en sede, parte a recolectar12210

Reglas de elegibilidad para pickups (OC / Traslados)

No todas las facturas aceptan recolecciones. El sistema bloquea la creación de pickups (OC o traslados) cuando se cumple cualquiera de estas condiciones:

Condición A — El tipo de factura no requiere recolección

Si el tipo de factura tiene pickups_needed = false, no se puede crear ninguna recolección para facturas de ese tipo, independientemente de las cantidades.

Error devuelto:

{
  "ok": false,
  "code": "SYNC_ERROR",
  "message": "Receipt 'FAC-10001' belongs to a type that does not require pickup collection (pickups_needed = false). A pickup (OC/transfer) cannot be created for this receipt."
}

Condición B — Todas las líneas ya tienen la cantidad en sede

Si la suma de quantity_pickup_required en todas las líneas es 0, los ítems ya están confirmados en la sede (quantity_base = quantity). No hay nada que recolectar.

Error devuelto:

{
  "ok": false,
  "code": "SYNC_ERROR",
  "message": "Receipt 'FAC-10001' does not require any external collection — all line quantities are already confirmed at the hub (quantity_pickup_required = 0 for all lines). A pickup (OC/transfer) cannot be created for this receipt."
}

Cuándo SÍ se puede crear una recolección

Condición¿Permite pickup?
pickups_needed = true y sum(quantity_pickup_required) > 0✅ Sí
pickups_needed = false❌ No
pickups_needed = true pero sum(quantity_pickup_required) = 0❌ No

Estados del proceso de recolección

El sistema calcula automáticamente el estado de recolección de una factura según el progreso de sus OCs/traslados:

EstadoCuándo ocurre
NOT_REQUIREDsum(quantity_pickup_required) = 0 — no se necesita recolección
PENDINGHay recolección pendiente y ninguna ha sido confirmada aún
IN_PROGRESSAl menos una recolección ha sido confirmada pero no todas completadas
COMPLETEquantity_pickup_collected >= quantity_pickup_required para todos los ítems

Estados de un dispatch

IDEstadoDescripción
1SIN_ASIGNARSin chofer/vehículo asignado
2ASIGNADAListo para iniciar
3EN_TRANSITOEn ruta
4COMPLETATodos los stops completados
5INCOMPLETATerminado con stops pendientes
6ANULADACancelado

El dispatch pasa automáticamente a COMPLETA (4) cuando se completa el último stop activo.


Caso A — Entrega directa (sin recolección)

Aplica cuando la factura tiene pickups_needed = false o todas sus líneas tienen quantity_base = quantity.

1. Crear factura (receipt)
2. Crear dispatch con stops: hub → receipt → hub
3. Iniciar dispatch (ASIGNADA → EN_TRANSITO)
4. Completar el stop del hub de inicio
5. Completar el stop de la factura (con líneas entregadas)
   → El dispatch se completa automáticamente

1. Crear la factura

POST /transportes/sync/receipts
{
  "source_id": "account.move:1001",
  "source_system": "external_erp",
  "receipt_code": "FAC-1001",
  "receipt_type_source_id": "smartfleet.receipt_type:2",
  "receipt_type_model": "smartfleet.receipt_type",
  "hub_source_id": "stock.warehouse:1",
  "hub_model": "stock.warehouse",
  "project_source_id": "project.project:9001",
  "project_model": "project.project",
  "main_account_name": "Distribuidora ABC",
  "recipient_name": "Ana Mora",
  "address": "San José, CR",
  "latitude": 9.93,
  "longitude": -84.14,
  "lines": [
    {
      "product_code": "00838",
      "quantity": 10,
      "quantity_base": 10,
      "quantity_pickup_required": 0
    }
  ]
}

quantity_base = quantity indica que los 10 ítems ya están confirmados en sede. No se necesita recolección.

2. Crear el dispatch

POST /transportes/sync/dispatch
{
  "hub_id": 1,
  "driver_id": 5,
  "vehicle_id": 12,
  "total_weight": 8.5,
  "user": "erp-sync",
  "dispatch_steps": {
    "1": {
      "order": 1,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    },
    "2": {
      "order": 2,
      "type": "receipt",
      "item": {
        "receipt_id": 1001,
        "receipt_code": "FAC-1001",
        "receipt_type_id": 2,
        "receipt_type_code": "FAC",
        "hub_id": 1,
        "latitude": 9.93,
        "longitude": -84.14
      }
    },
    "3": {
      "order": 3,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    }
  }
}

3. Iniciar el dispatch

POST /transportes/sync/dispatch/start
{
  "dispatch_id": 501
}

4. Iniciar y completar el stop hub de inicio

POST /transportes/sync/dispatch/task/start
{
  "driver_id": 5,
  "dispatch_stop_id": 1201
}
POST /transportes/sync/dispatch/task/complete
{
  "driver_id": 5,
  "dispatch_stop_id": 1201
}

5. Completar el stop de la factura

POST /transportes/sync/dispatch/task/complete
{
  "driver_id": 5,
  "dispatch_stop_id": 1202,
  "lines": [
    {
      "product_code": "00838",
      "expected_quantity": 10,
      "delivered_quantity": 10
    }
  ],
  "notes": "Entrega sin novedad"
}

Al completar el último stop activo, el dispatch pasa automáticamente a COMPLETA (4).


Caso B — Entrega con OC (Orden de Compra)

Aplica cuando parte o toda la mercancía debe ser recolectada del proveedor antes de la entrega al cliente. Requiere pickups_needed = true y quantity_pickup_required > 0 en al menos una línea.

1. Crear factura con quantity_pickup_required > 0
2. Crear OC vinculada a la factura
3. Crear dispatch con stops: hub → REC_OC → ENT_FAC → hub
4. Iniciar dispatch
5. Ejecutar el stop de recolección (task/start)
6. Confirmar items recolectados (pickup/status CONFIRM → COMPLETE)
7. Completar el stop de recolección (task/complete)
8. Completar el stop de la factura (task/complete con líneas)
   → dispatch se completa automáticamente

1. Crear la factura con recolección pendiente

POST /transportes/sync/receipts
{
  "source_id": "account.move:1002",
  "source_system": "external_erp",
  "receipt_code": "FAC-1002",
  "receipt_type_source_id": "smartfleet.receipt_type:1",
  "hub_source_id": "stock.warehouse:1",
  "main_account_name": "Empresa XYZ",
  "recipient_name": "Luis Rodríguez",
  "address": "Alajuela, CR",
  "latitude": 10.01,
  "longitude": -84.21,
  "lines": [
    {
      "product_code": "00838",
      "quantity": 10,
      "quantity_base": 2,
      "quantity_pickup_required": 8
    }
  ]
}

2 unidades ya en sede, 8 deben recolectarse del proveedor.

2. Crear la OC vinculada

POST /transportes/sync/pickups
{
  "source_id": "purchase.order:371",
  "source_system": "external_erp",
  "pickup_type": "OC",
  "document_code": "OC-371",
  "receipt_code": "FAC-1002",
  "supplier_source_id": "res.partner:7001",
  "supplier_model": "res.partner",
  "lines": [
    {
      "product_code": "00838",
      "quantity": 8,
      "warehouse": "WH/Stock"
    }
  ]
}

3. Crear el dispatch con stop de recolección

POST /transportes/sync/dispatch
{
  "hub_id": 1,
  "driver_id": 5,
  "vehicle_id": 12,
  "total_weight": 12.0,
  "user": "erp-sync",
  "dispatch_steps": {
    "1": {
      "order": 1,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    },
    "2": {
      "order": 2,
      "type": "pickup",
      "item": {
        "pickup_type": "OC",
        "document_id": 371,
        "receipt_code": "FAC-1002",
        "receipt_id": 1002,
        "origin_description": "Proveedor ABC",
        "latitude": 9.93276,
        "longitude": -84.08577
      }
    },
    "3": {
      "order": 3,
      "type": "receipt",
      "item": {
        "receipt_id": 1002,
        "receipt_code": "FAC-1002",
        "receipt_type_id": 1,
        "receipt_type_code": "FAC",
        "hub_id": 1,
        "latitude": 10.01,
        "longitude": -84.21
      }
    },
    "4": {
      "order": 4,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    }
  }
}

4. Iniciar el dispatch

POST /transportes/sync/dispatch/start
{
  "dispatch_id": 502
}

5. Iniciar el stop de recolección

POST /transportes/sync/dispatch/task/start
{
  "driver_id": 5,
  "dispatch_stop_id": 1210
}

6. Registrar los ítems recolectados

Primero confirmar (CONFIRM) que el item fue recolectado:

POST /transportes/sync/dispatch/pickup/status
{
  "driver_id": 5,
  "dispatch_stop_id": 1210,
  "action": "CONFIRM",
  "lines": [
    {
      "product_code": "00838",
      "collected_quantity": 8
    }
  ]
}

Luego marcar la recolección como completada (COMPLETE):

POST /transportes/sync/dispatch/pickup/status
{
  "driver_id": 5,
  "dispatch_stop_id": 1210,
  "action": "COMPLETE"
}

7. Completar el stop de recolección

POST /transportes/sync/dispatch/task/complete
{
  "driver_id": 5,
  "dispatch_stop_id": 1210
}

8. Completar el stop de la factura

POST /transportes/sync/dispatch/task/complete
{
  "driver_id": 5,
  "dispatch_stop_id": 1211,
  "lines": [
    {
      "product_code": "00838",
      "expected_quantity": 10,
      "delivered_quantity": 10
    }
  ],
  "notes": "Entrega completa"
}

Caso C — Entrega con traslado (TRANSFER)

Aplica cuando la mercancía debe trasladarse desde otra sede antes de la entrega. El flujo es idéntico al Caso B, cambiando pickup_type: "OC" por pickup_type: "TRANSFER" y usando hub_source_id en lugar de supplier_source_id.

1. Crear la factura

Igual que el Caso B, con quantity_pickup_required > 0.

2. Crear el traslado

POST /transportes/sync/transfers
{
  "source_id": "stock.picking:470",
  "source_system": "external_erp",
  "document_code": "TR-470",
  "receipt_code": "FAC-1003",
  "hub_source_id": "stock.warehouse:2",
  "hub_model": "stock.warehouse",
  "source_warehouse": "ALAJUELA/Stock",
  "target_warehouse": "CEDI/Stock",
  "lines": [
    {
      "product_code": "00838",
      "quantity": 8,
      "source_warehouse": "ALAJUELA/Stock",
      "target_warehouse": "CEDI/Stock"
    }
  ]
}

3. Crear el dispatch

En dispatch_steps, usar type: "transfer" para el stop de traslado:

dispatch_steps — stop de traslado
{
  "2": {
    "order": 2,
    "type": "transfer",
    "item": {
      "pickup_type": "TRANSFER",
      "document_id": 470,
      "receipt_code": "FAC-1003",
      "receipt_id": 1003,
      "origin_description": "Sede Alajuela",
      "latitude": 10.01064,
      "longitude": -84.21583
    }
  }
}

4–8. Ejecutar el ciclo de vida

Exactamente igual que el Caso B, usando los dispatch_stop_id correctos para el stop de traslado y el stop de factura.


Caso D — Devolución después de entrega

Las devoluciones solo pueden crearse cuando la factura está en estado COMPLETA (4) — es decir, fue entregada al cliente.

1. (Prerequisito) Factura en status_id = 4 tras completar su dispatch
2. Crear la devolución (return)
3. Crear un nuevo dispatch de recolección de devolución
4. Ejecutar el ciclo de vida del dispatch de devolución

1. Crear la devolución

POST /transportes/sync/returns
{
  "source_id": "return.request:84",
  "source_system": "external_erp",
  "receipt_source_id": "account.move:1001",
  "receipt_model": "account.move",
  "requester": "Cliente",
  "classification_description": "Producto dañado",
  "reason": "Artículo quebrado en el transporte",
  "lines": [
    {
      "product_code": "00838",
      "quantity": 2
    }
  ]
}

Response:

{
  "ok": true,
  "code": "SYNC_OK",
  "data": 84,
  "meta": { "upsert_action": "created" }
}

2. Crear el dispatch de recolección de devolución

POST /transportes/sync/dispatch
{
  "hub_id": 1,
  "driver_id": 5,
  "vehicle_id": 12,
  "total_weight": 2.0,
  "user": "erp-sync",
  "dispatch_steps": {
    "1": {
      "order": 1,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    },
    "2": {
      "order": 2,
      "type": "return",
      "item": {
        "return_id": 84,
        "receipt_id": 1001,
        "latitude": 9.93,
        "longitude": -84.14
      }
    },
    "3": {
      "order": 3,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    }
  }
}

3. Ejecutar el ciclo de vida

Iniciar dispatch de devolución
{ "dispatch_id": 503 }
Completar el stop de la devolución
{
  "driver_id": 5,
  "dispatch_stop_id": 1230,
  "materials": [
    {
      "product_code": "00838",
      "quantity": 2
    }
  ],
  "notes": "Producto recogido sin daños adicionales"
}

Caso E — Solo recolección (PICKUP_ONLY)

Dispatch cuyo único objetivo es recolectar mercancía de proveedores o sedes, sin entregas a clientes en esa ruta. Útil cuando las recolecciones se planifican por separado de las entregas.

1. Tener OCs / traslados pendientes
2. Crear dispatch con dispatch_mode: "PICKUP_ONLY"
3. Iniciar dispatch
4. Confirmar recolecciones (pickup/status)
5. Completar stops
POST /transportes/sync/dispatch — PICKUP_ONLY
{
  "hub_id": 1,
  "driver_id": 5,
  "vehicle_id": 12,
  "total_weight": 20.0,
  "user": "erp-sync",
  "dispatch_mode": "PICKUP_ONLY",
  "dispatch_steps": {
    "1": {
      "order": 1,
      "type": "hub",
      "item": { "hub_id": 2, "name": "Alajuela", "latitude": 10.01064, "longitude": -84.21583 }
    },
    "2": {
      "order": 2,
      "type": "pickup",
      "item": {
        "pickup_type": "OC",
        "document_id": 371,
        "receipt_code": "FAC-1002",
        "receipt_id": 1002,
        "origin_description": "Proveedor ABC",
        "latitude": 9.93276,
        "longitude": -84.08577
      }
    },
    "3": {
      "order": 3,
      "type": "hub",
      "item": { "hub_id": 1, "name": "CEDI", "latitude": 9.9797, "longitude": -84.1408 }
    }
  }
}

Nota: Si se intenta crear otro dispatch PICKUP_ONLY para los mismos pickups ya procesados, el backend bloqueará la operación.


Cancelar un stop (y reintentar)

Cuando un stop no puede completarse (dirección incorrecta, cliente ausente, etc.), se cancela. Esto revierte el dispatch a ASIGNADA (2) para que pueda reprogramarse.

POST /transportes/sync/dispatch/task/cancel
{
  "driver_id": 5,
  "dispatch_stop_id": 1202,
  "reason": "Cliente no se encontraba en la dirección indicada"
}

Efectos:

  • El stop queda con estado ANULADA.
  • El documento vinculado (factura, OC, devolución) vuelve a su estado anterior.
  • El dispatch regresa a ASIGNADA (2).

Luego de corregir la situación, se puede volver a iniciar el dispatch y reintentar el stop.


Entrega parcial (con motivo de no entrega)

Cuando no se puede entregar la cantidad completa, se registra la cantidad entregada y la no entregada junto con un motivo:

POST /transportes/sync/dispatch/task/complete — entrega parcial
{
  "driver_id": 5,
  "dispatch_stop_id": 1202,
  "lines": [
    {
      "product_code": "00838",
      "expected_quantity": 10,
      "delivered_quantity": 6,
      "not_delivered_quantity": 4,
      "reason_code": "CLIENTE_AUSENTE",
      "reason": "Cliente no estaba al momento de la entrega"
    }
  ],
  "notes": "Entrega parcial. Se dejó aviso."
}

Si delivered_quantity < expected_quantity para alguna línea, el documento queda en estado INCOMPLETA (5). Si todas las líneas son 0 entregadas, también queda INCOMPLETA.


Consultar estado del dispatch

En cualquier momento del flujo se puede consultar el estado completo de un dispatch por su source_id externo:

GET/transportes/sync/dispatch/{source_id}

Retorna el estado completo del dispatch, sus stops y sus documentos vinculados.

GET /transportes/sync/dispatch/guia.despacho:501?source_system=external_erp

La respuesta incluye el dispatch_stop_id de cada parada, necesario para los endpoints de task/start, task/complete, task/cancel y pickup/status.


Errores comunes y cómo manejarlos

ErrorCausaSolución
pickups_needed = falseSe intentó crear una OC/traslado para una factura cuyo tipo no requiere recolecciónNo crear recolección para esa factura
quantity_pickup_required = 0Todas las líneas ya tienen quantity_base = quantityNo se necesita recolección; ir directo al dispatch de entrega
SYNC_NOT_FOUND al iniciar dispatchEl dispatch_source_id + source_system no coincide con ningún dispatchVerificar que el dispatch fue creado correctamente
Dispatch is not in ASIGNADA statusSe intenta iniciar un dispatch que ya está EN_TRANSITO o COMPLETAConsultar el estado actual con el endpoint GET
driver_id not assignedSe intenta iniciar un dispatch sin chofer asignadoAsignar chofer/vehículo antes de iniciar
Missing related recordsDependencia no sincronizada (producto, proveedor, sede, etc.)Sincronizar primero las entidades faltantes del catálogo
Cannot create return: receipt not COMPLETASe intenta crear una devolución antes de que la factura sea entregadaCompletar el dispatch de entrega antes de crear el return
already associated with another sourceEl código de documento ya existe en el sistema con otro source_idRevisar si el registro ya fue creado previamente

Resumen del flujo completo (Caso B como ejemplo canónico)

SETUP (una vez por entidad maestra)
─────────────────────────────────────────────────────────────
POST /sync/receipts/types         → tipo de factura con pickups_needed=true
POST /sync/products               → catálogo de productos
POST /sync/hubs                   → sedes
POST /sync/suppliers              → proveedores
POST /sync/drivers                → choferes
POST /sync/vehicles               → vehículos

POR OPERACIÓN
─────────────────────────────────────────────────────────────
 1. POST /sync/receipts            → receipt_id: 1001 (con quantity_pickup_required > 0)
 2. POST /sync/pickups             → purchase_order_id: 371  (OC vinculada)
 3. POST /sync/dispatch            → dispatch_id: 501 (con stops hub→OC→FAC→hub)

CICLO DE VIDA DEL DISPATCH
─────────────────────────────────────────────────────────────
 4. POST /sync/dispatch/start                      ASIGNADA → EN_TRANSITO
 5. POST /sync/dispatch/task/start    (hub inicio)
 6. POST /sync/dispatch/task/complete (hub inicio)
 7. POST /sync/dispatch/task/start    (stop OC)
 8. POST /sync/dispatch/pickup/status  action=CONFIRM  (registra items recolectados)
 9. POST /sync/dispatch/pickup/status  action=COMPLETE (cierra recolección)
10. POST /sync/dispatch/task/complete (stop OC)
11. POST /sync/dispatch/task/start    (stop FAC)
12. POST /sync/dispatch/task/complete (stop FAC, con lines de cantidades entregadas)
13. POST /sync/dispatch/task/start    (hub final)
14. POST /sync/dispatch/task/complete (hub final)
    → dispatch auto-completa a COMPLETA (4)

POST-ENTREGA (opcional)
─────────────────────────────────────────────────────────────
15. POST /sync/returns             → return_id: 84
16. POST /sync/dispatch            → dispatch_id: 510 (stop REC_DEV)
17. POST /sync/dispatch/start + task/start + task/complete
    → dispatch de devolución completo

Para identificar los dispatch_stop_id de cada parada, consultar GET /sync/dispatch/{source_id} después del paso 3.