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_linesEl 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.
| Campo | Descripción |
|---|---|
quantity | Cantidad total del ítem en la factura |
quantity_base | Cantidad ya confirmada en la sede; no requiere recolección externa |
quantity_pickup_required | Cantidad que debe ser recolectada externamente (OC o traslado) |
quantity_pickup_collected | Cantidad efectivamente recolectada hasta ahora |
quantity_prepared | Cantidad lista y preparada para despacho en bodega |
quantity_delivered | Cantidad entregada al cliente |
quantity_not_delivered | Cantidad no entregada en la última guía |
quantity_returned | Cantidad devuelta por el cliente |
quantity_not_collected | Cantidad no recolectada (recolección fallida) |
Restricción clave:
quantity = quantity_base + quantity_pickup_requiredEjemplos:
| Escenario | quantity | quantity_base | quantity_pickup_required |
|---|---|---|---|
| Todo en sede, sin recolección | 10 | 10 | 0 |
| Todo debe recolectarse | 10 | 0 | 10 |
| Parte en sede, parte a recolectar | 12 | 2 | 10 |
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:
| Estado | Cuándo ocurre |
|---|---|
NOT_REQUIRED | sum(quantity_pickup_required) = 0 — no se necesita recolección |
PENDING | Hay recolección pendiente y ninguna ha sido confirmada aún |
IN_PROGRESS | Al menos una recolección ha sido confirmada pero no todas completadas |
COMPLETE | quantity_pickup_collected >= quantity_pickup_required para todos los ítems |
Estados de un dispatch
| ID | Estado | Descripción |
|---|---|---|
| 1 | SIN_ASIGNAR | Sin chofer/vehículo asignado |
| 2 | ASIGNADA | Listo para iniciar |
| 3 | EN_TRANSITO | En ruta |
| 4 | COMPLETA | Todos los stops completados |
| 5 | INCOMPLETA | Terminado con stops pendientes |
| 6 | ANULADA | Cancelado |
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áticamente1. Crear la factura
{
"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 = quantityindica que los 10 ítems ya están confirmados en sede. No se necesita recolección.
2. Crear el 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
{
"dispatch_id": 501
}4. Iniciar y completar el stop hub de inicio
{
"driver_id": 5,
"dispatch_stop_id": 1201
}{
"driver_id": 5,
"dispatch_stop_id": 1201
}5. Completar el stop de la factura
{
"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áticamente1. Crear la factura con recolección pendiente
{
"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
{
"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
{
"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
{
"dispatch_id": 502
}5. Iniciar el stop de recolección
{
"driver_id": 5,
"dispatch_stop_id": 1210
}6. Registrar los ítems recolectados
Primero confirmar (CONFIRM) que el item fue recolectado:
{
"driver_id": 5,
"dispatch_stop_id": 1210,
"action": "CONFIRM",
"lines": [
{
"product_code": "00838",
"collected_quantity": 8
}
]
}Luego marcar la recolección como completada (COMPLETE):
{
"driver_id": 5,
"dispatch_stop_id": 1210,
"action": "COMPLETE"
}7. Completar el stop de recolección
{
"driver_id": 5,
"dispatch_stop_id": 1210
}8. Completar el stop de la factura
{
"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
{
"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:
{
"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ón1. Crear la devolución
{
"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
{
"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
{ "dispatch_id": 503 }{
"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{
"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_ONLYpara 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.
{
"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:
{
"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_quantitypara alguna línea, el documento queda en estadoINCOMPLETA (5). Si todas las líneas son 0 entregadas, también quedaINCOMPLETA.
Consultar estado del dispatch
En cualquier momento del flujo se puede consultar el estado completo de un dispatch por su source_id externo:
Retorna el estado completo del dispatch, sus stops y sus documentos vinculados.
GET /transportes/sync/dispatch/guia.despacho:501?source_system=external_erpLa 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
| Error | Causa | Solución |
|---|---|---|
pickups_needed = false | Se intentó crear una OC/traslado para una factura cuyo tipo no requiere recolección | No crear recolección para esa factura |
quantity_pickup_required = 0 | Todas las líneas ya tienen quantity_base = quantity | No se necesita recolección; ir directo al dispatch de entrega |
SYNC_NOT_FOUND al iniciar dispatch | El dispatch_source_id + source_system no coincide con ningún dispatch | Verificar que el dispatch fue creado correctamente |
Dispatch is not in ASIGNADA status | Se intenta iniciar un dispatch que ya está EN_TRANSITO o COMPLETA | Consultar el estado actual con el endpoint GET |
driver_id not assigned | Se intenta iniciar un dispatch sin chofer asignado | Asignar chofer/vehículo antes de iniciar |
Missing related records | Dependencia no sincronizada (producto, proveedor, sede, etc.) | Sincronizar primero las entidades faltantes del catálogo |
Cannot create return: receipt not COMPLETA | Se intenta crear una devolución antes de que la factura sea entregada | Completar el dispatch de entrega antes de crear el return |
already associated with another source | El código de documento ya existe en el sistema con otro source_id | Revisar 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 completoPara identificar los
dispatch_stop_idde cada parada, consultarGET /sync/dispatch/{source_id}después del paso 3.