Skip to main content

Guías de Remisión

Las guías de remisión electrónicas (código 09) son documentos obligatorios para el traslado de bienes en Perú. El sistema integra con la API GRE (Guía de Remisión Electrónica) de SUNAT para envío asíncrono con consulta de tickets.

Tipos de Guías

Guía Remitente

Emitida por el dueño de la mercancía para autorizar el traslado

Guía Transportista

Emitida por la empresa de transporte que realiza el traslado
Esta documentación cubre las guías de remitente. Las guías de transportista se gestionan en el módulo GuiaRemisionTransportistaController.

Motivos de Traslado

SUNAT define motivos específicos para el traslado de bienes:
Traslado por venta de bienes. Se vincula a una factura o boleta.
Traslado de bienes adquiridos.
Movimiento interno entre almacenes o sucursales.
Traslado de bienes importados.
Traslado de bienes para exportación.
Otros motivos no clasificados.
Envío a terceros para procesamiento.

Modalidades de Transporte

01 - Transporte Público

Se contrata una empresa de transporte externa. Requiere datos del transportista.

02 - Transporte Privado

Transporte propio. Requiere datos del conductor y vehículo.

Flujo de Trabajo

1

Datos del Destinatario

Define quién recibe la mercancía:
  • Tipo de documento: DNI (1), RUC (6), Carnet de extranjería (4)
  • Número de documento
  • Nombre o razón social
  • Dirección de llegada
  • Ubigeo (opcional, se usa 150101 por defecto)
2

Motivo de Traslado

Selecciona el motivo según catálogo SUNAT y describe brevemente:
// GuiaRemisionController.php:149
'motivo_traslado' => $request->motivo_traslado,  // Ej: '01'
'descripcion_motivo' => $request->descripcion_motivo,
3

Datos del Traslado

Configura detalles del transporte:
  • Fecha de traslado: Cuándo se realizará el movimiento
  • Peso total: En kilogramos (KGM)
  • Punto de partida: Dirección y ubigeo de origen
  • Punto de llegada: Dirección del destinatario
4

Modalidad de Transporte

Elige público o privado:Para transporte público (01):
  • Tipo doc transportista (6 para RUC)
  • RUC del transportista
  • Razón social
  • Número MTC (opcional)
Para transporte privado (02):
  • Tipo doc conductor (1 para DNI)
  • DNI del conductor
  • Nombres y apellidos
  • Número de licencia
  • Placa del vehículo
  • Indicador M1/L (vehículos ligeros)
5

Detalle de Bienes

Lista los productos a trasladar:
'detalles' => [
    [
        'id_producto' => 15,
        'codigo' => 'PROD-001',
        'descripcion' => 'Laptop HP 15"',
        'cantidad' => 5,
        'unidad' => 'NIU'  // Unidad SUNAT
    ]
]
6

Generar XML

El sistema automáticamente:
  • Asigna serie T001 y número correlativo
  • Genera XML usando Greenter
  • Firma digitalmente con certificado
  • Guarda en storage/app/sunat/xml/{ruc}/
7

Enviar a SUNAT (GRE API)

Envío asíncrono con tickets:
  1. Se envía el XML a la API REST de SUNAT
  2. SUNAT devuelve un número de ticket
  3. Se consulta el ticket periódicamente
  4. Cuando está listo, se descarga el CDR

Validaciones Especiales

Vehículos M1/L (Categoría Ligera)

Si el vehículo es M1 o L, los datos del conductor y placa son opcionales:
// GuiaRemisionController.php:78-96
$rules['vehiculo_m1l'] = 'nullable|boolean';

if ($request->mod_transporte === '02') {
    if ($request->boolean('vehiculo_m1l')) {
        // M1/L: todos los campos del conductor son opcionales
        $rules['conductor_tipo_doc'] = 'nullable|string|max:1';
        $rules['conductor_documento'] = 'nullable|string|max:15';
        $rules['vehiculo_placa'] = 'nullable|string|max:10';
    } else {
        // Sin M1/L: todos obligatorios
        $rules['conductor_tipo_doc'] = 'required|string|max:1';
        $rules['conductor_documento'] = 'required|string|max:15';
        $rules['vehiculo_placa'] = 'required|string|max:10';
    }
}

Dirección de Partida

Si no se especifica, usa la dirección de la empresa:
// GuiaRemisionController.php:132-134
$ubigeoPartida = $request->ubigeo_partida ?: ($empresa->ubigeo ?: '150101');
$dirPartida = $request->dir_partida ?: ($empresa->direccion ?: '');

Implementación Backend

Crear Guía de Remisión

// app/Http/Controllers/GuiaRemisionController.php:40-205

public function store(Request $request): JsonResponse
{
    $rules = [
        'id_venta' => 'nullable|exists:ventas,id_venta',
        'destinatario_tipo_doc' => 'required|in:1,4,6',
        'destinatario_documento' => 'required|string|max:15',
        'destinatario_nombre' => 'required|string|max:255',
        'destinatario_direccion' => 'required|string|max:500',
        'motivo_traslado' => 'required|string|max:2',
        'mod_transporte' => 'required|in:01,02',
        'fecha_traslado' => 'required|date',
        'peso_total' => 'required|numeric|min:0.001',
        'detalles' => 'required|array|min:1',
        'detalles.*.descripcion' => 'required|string',
        'detalles.*.cantidad' => 'required|numeric|min:0.001',
    ];

    // Validaciones dinámicas según modalidad
    if ($request->mod_transporte === '01') {
        $rules['transportista_tipo_doc'] = 'required|string|max:1';
        $rules['transportista_documento'] = 'required|string|max:15';
        $rules['transportista_nombre'] = 'required|string|max:255';
    } else {
        // Transporte privado: validar según M1/L
        if (!$request->boolean('vehiculo_m1l')) {
            $rules['conductor_tipo_doc'] = 'required|string|max:1';
            $rules['conductor_documento'] = 'required|string|max:15';
            $rules['vehiculo_placa'] = 'required|string|max:10';
        }
    }

    $request->validate($rules);

    return DB::transaction(function () use ($request) {
        $idEmpresa = $request->user()->id_empresa;
        $empresa = Empresa::findOrFail($idEmpresa);

        // Obtener próximo número correlativo
        $ultimoNumero = GuiaRemision::where('serie', 'T001')
            ->where('id_empresa', $idEmpresa)
            ->max('numero') ?? 0;

        $numeroBase = DB::table('documentos_empresas')
            ->where('serie', 'T001')
            ->value('numero') ?? 0;

        $ultimoNumero = max($ultimoNumero, $numeroBase);

        // Crear guía
        $guia = GuiaRemision::create([
            'id_empresa' => $idEmpresa,
            'id_usuario' => $request->user()->id,
            'id_venta' => $request->id_venta,
            'serie' => 'T001',
            'numero' => $ultimoNumero + 1,
            'fecha_emision' => now()->toDateString(),
            'destinatario_tipo_doc' => $request->destinatario_tipo_doc,
            'destinatario_documento' => $request->destinatario_documento,
            'destinatario_nombre' => $request->destinatario_nombre,
            'motivo_traslado' => $request->motivo_traslado,
            'descripcion_motivo' => $request->descripcion_motivo,
            'mod_transporte' => $request->mod_transporte,
            'fecha_traslado' => $request->fecha_traslado,
            'peso_total' => $request->peso_total,
            'und_peso_total' => $request->und_peso_total ?? 'KGM',
            'ubigeo_partida' => $request->ubigeo_partida ?: $empresa->ubigeo,
            'dir_partida' => $request->dir_partida ?: $empresa->direccion,
            'ubigeo_llegada' => $request->destinatario_ubigeo ?: '150101',
            'dir_llegada' => $request->destinatario_direccion,
            'transportista_tipo_doc' => $request->transportista_tipo_doc,
            'transportista_documento' => $request->transportista_documento,
            'transportista_nombre' => $request->transportista_nombre,
            'conductor_tipo_doc' => $request->conductor_tipo_doc,
            'conductor_documento' => $request->conductor_documento,
            'conductor_nombres' => $request->conductor_nombres,
            'vehiculo_placa' => $request->vehiculo_placa,
            'vehiculo_m1l' => $request->boolean('vehiculo_m1l'),
            'estado' => 'pendiente',
        ]);

        // Guardar detalles
        foreach ($request->detalles as $detalle) {
            GuiaRemisionDetalle::create([
                'id_guia' => $guia->id,
                'id_producto' => $detalle['id_producto'] ?? null,
                'codigo' => $detalle['codigo'] ?? null,
                'descripcion' => $detalle['descripcion'],
                'cantidad' => $detalle['cantidad'],
                'unidad' => $detalle['unidad'] ?? 'NIU',
            ]);
        }

        // Generar XML
        $resultado = $this->sunatService->generarGuiaRemisionXml($guia);

        return response()->json([
            'success' => true,
            'data' => $guia,
            'xml' => $resultado,
        ], 201);
    });
}

Enviar a SUNAT (GRE API)

El envío es asíncrono usando la API REST:
// GuiaRemisionController.php:207-234
public function enviar(int $id, Request $request): JsonResponse
{
    $guia = GuiaRemision::where('id_empresa', $request->user()->id_empresa)
        ->findOrFail($id);

    if (!$guia->nombre_xml) {
        return response()->json([
            'success' => false,
            'message' => 'La guía no tiene XML generado'
        ], 400);
    }

    try {
        // Envío asíncrono con ticket
        $resultado = $this->sunatService->enviarGuiaRemision($guia);
        return response()->json($resultado);
    } catch (\Exception $e) {
        Log::error('Error al enviar guía', [
            'guia_id' => $id,
            'error' => $e->getMessage()
        ]);
        
        return response()->json([
            'success' => false,
            'message' => 'Error al enviar: ' . $e->getMessage()
        ], 500);
    }
}

Consultar Ticket

Después del envío, se debe consultar el ticket hasta que SUNAT procese la guía:
// GuiaRemisionController.php:236-255
public function consultarTicket(int $id, Request $request): JsonResponse
{
    $guia = GuiaRemision::where('id_empresa', $request->user()->id_empresa)
        ->findOrFail($id);

    try {
        $resultado = $this->sunatService->consultarTicketGuia($guia);
        return response()->json($resultado);
    } catch (\Exception $e) {
        Log::error('Error al consultar ticket', [
            'guia_id' => $id,
            'error' => $e->getMessage()
        ]);
        
        return response()->json([
            'success' => false,
            'message' => 'Error al consultar: ' . $e->getMessage()
        ], 500);
    }
}

Integración con SUNAT

Diferencias con SOAP

A diferencia de facturas/boletas que usan SOAP sincrónico, las guías usan GRE API REST con flujo asíncrono:
1

Autenticación OAuth

Se obtiene un token de acceso usando credenciales client_id y client_secret:
POST https://api-seguridad.sunat.gob.pe/v1/clientessol/{client_id}/oauth2/token/
grant_type=client_credentials
2

Envío de XML

Se envía el XML codificado en base64:
POST https://api-cpe.sunat.gob.pe/v1/contribuyente/gem/comprobantes/{nombre_xml}
Authorization: Bearer {token}
Content-Type: application/json

{
  "archivo": {
    "nomArchivo": "{nombre_xml}.xml",
    "arcGreZip": "{base64_zip}",
    "hashZip": "{hash}"
  }
}
3

Recepción de Ticket

SUNAT devuelve un número de ticket:
{
  "numTicket": "1234567890123",
  "codRespuesta": "0",
  "desRespuesta": "El comprobante fue registrado correctamente"
}
4

Consulta de Ticket

Se consulta periódicamente hasta obtener el CDR:
GET https://api-cpe.sunat.gob.pe/v1/contribuyente/gem/comprobantes/envios/{ticket}
Authorization: Bearer {token}
5

Descarga de CDR

Cuando el estado es “03” (procesado), se descarga el CDR en base64.
Importante: La API GRE requiere credenciales OAuth diferentes a las credenciales SOL (SOAP). Debes configurar SUNAT_GRE_CLIENT_ID y SUNAT_GRE_CLIENT_SECRET en tu archivo .env.

Vinculación con Ventas

Las guías pueden vincularse a una venta:
'id_venta' => $request->id_venta,  // Opcional
Esto permite:
  • Autocompletar datos del cliente desde la venta
  • Cargar automáticamente los productos vendidos
  • Mantener trazabilidad entre comprobante y guía

Catálogo de Motivos

Endpoint para obtener motivos de traslado:
// GuiaRemisionController.php:280-287
public function motivos(): JsonResponse
{
    $motivos = MotivoTraslado::where('estado', true)
        ->orderBy('codigo')
        ->get();

    return response()->json($motivos);
}

Búsqueda de Ubigeos

Para facilitar la entrada de direcciones:
// GuiaRemisionController.php:348-362
public function ubigeos(Request $request): JsonResponse
{
    $search = $request->get('q', '');

    $query = DB::table('ubigeo_inei');

    if ($search) {
        $query->where('nombre', 'like', "%{$search}%")
            ->orWhere('id_ubigeo', 'like', "%{$search}%");
    }

    $ubigeos = $query->limit(20)->get();

    return response()->json($ubigeos);
}

Descarga de Documentos

Descargar CDR

// GuiaRemisionController.php:289-306
public function cdr(int $id, Request $request)
{
    $guia = GuiaRemision::where('id_empresa', $request->user()->id_empresa)
        ->findOrFail($id);

    if (!$guia->cdr_url) {
        return response()->json(['message' => 'CDR no disponible'], 404);
    }

    $cdrPath = storage_path("app/{$guia->cdr_url}");
    
    if (!file_exists($cdrPath)) {
        return response()->json(['message' => 'Archivo no encontrado'], 404);
    }

    return response()->download($cdrPath, "R-{$guia->serie}-{$guia->numero}.zip");
}

Descargar XML

// GuiaRemisionController.php:308-327
public function xml(string $nombre)
{
    $nombreXml = preg_replace('/\.xml$/i', '', $nombre);

    $guia = GuiaRemision::where('nombre_xml', $nombreXml)->first();

    if (!$guia || !$guia->xml_url) {
        return response()->json(['message' => 'XML no encontrado'], 404);
    }

    $xmlPath = storage_path("app/{$guia->xml_url}");
    
    if (!file_exists($xmlPath)) {
        return response()->json(['message' => 'Archivo no encontrado'], 404);
    }

    return response()->file($xmlPath, [
        'Content-Type' => 'application/xml',
        'Content-Disposition' => "inline; filename=\"{$nombreXml}.xml\"",
    ]);
}

Tablas de Base de Datos

TablaDescripciónCampos Clave
guia_remisionsEncabezado de la guíaid, serie, numero, mod_transporte, estado, ticket_sunat
detalle_guia_remisionsBienes a trasladarid_guia, id_producto, descripcion, cantidad, unidad
motivo_trasladoCatálogo de motivos SUNATid, codigo, descripcion, estado
ubigeo_ineiTabla de ubigeos INEIid_ubigeo, nombre, departamento, provincia, distrito

Endpoints API

Listar Guías

GET /api/guias-remision

Crear Guía

POST /api/guias-remision

Enviar a SUNAT

POST /api/guias-remision/:id/enviar

Consultar Ticket

POST /api/guias-remision/:id/ticket

Buenas Prácticas

Recomendación: Implementa un job programado que consulte automáticamente los tickets pendientes cada 5 minutos hasta obtener el CDR.
Las guías deben emitirse antes del traslado de los bienes. Es infracción tributaria realizar el traslado sin guía o con datos incorrectos.
Para traslados dentro de Lima Metropolitana, el ubigeo por defecto es 150101.

Próximos Pasos

Guías Transportista

Aprende sobre guías de transportista

Configurar GRE

Configuración de credenciales GRE API

Consultar CDR

Verifica el estado en SUNAT

Ubigeos INEI

Catálogo completo de ubigeos

Build docs developers (and LLMs) love