Skip to main content

Introducción

Las Notas de Crédito permiten anular o modificar parcialmente un comprobante de pago ya emitido. Son documentos electrónicos que se envían a SUNAT y están regulados por normativa fiscal peruana.
Las Notas de Crédito NO anulan el comprobante original, sino que lo modifican. Para anulación completa, se debe usar Comunicación de Baja. Ver Anular Documento.

¿Cuándo usar Nota de Crédito?

Casos de uso:
  • Devolución total o parcial de productos
  • Error en precio o cantidad
  • Descuento posterior a la emisión
  • Bonificación o ajuste comercial
  • Anulación de operación (motivo 01)

Motivos SUNAT

La normativa peruana define 13 motivos oficiales para emitir Notas de Crédito:
CódigoDescripciónUso común
01Anulación de la operaciónCancelación total de venta
02Anulación por error en el RUCCliente incorrecto
03Corrección por error en la descripciónProducto mal descrito
04Descuento globalDescuento posterior
05Descuento por ítemDescuento en productos específicos
06Devolución totalDevolución de todos los productos
07Devolución por ítemDevolución parcial
08BonificaciónRegalo o bono posterior
09Disminución en el valorAjuste de precio
10Otros conceptosOtros casos
11Ajuste de operaciones de exportaciónExportaciones
12Ajuste afecto al IVAPImpuesto al arroz
13Corrección del monto neto pendiente de pagoCréditos
Tabla en BD: motivo_nota con campo tipo='NC'
// Modelo: app/Models/MotivoNota.php
class MotivoNota extends Model
{
    protected $table = 'motivo_nota';
    
    protected $fillable = [
        'codigo_sunat',  // '01', '02', ..., '13'
        'descripcion',
        'tipo',          // 'NC' o 'ND'
        'estado',        // boolean
    ];
}

Estructura de Nota de Crédito

Tabla: notas_credito

[
    'id',
    'id_venta',                 // FK a ventas (documento afectado)
    'motivo_id',                // FK a motivo_nota
    'serie',                    // FC01 (facturas) o BC01 (boletas)
    'numero',                   // Correlativo por serie
    'tipo_doc_afectado',        // '01' (factura) o '03' (boleta)
    'serie_num_afectado',       // 'F001-000123'
    'descripcion_motivo',       // Texto libre adicional
    'monto_subtotal',           // Base imponible
    'monto_igv',                // IGV
    'monto_total',              // Total de la NC
    'moneda',                   // PEN, USD
    'fecha_emision',
    'nombre_xml',               // Nombre del archivo XML generado
    'xml_url',                  // Ruta del XML en storage
    'cdr_url',                  // Ruta del CDR de SUNAT
    'estado',                   // pendiente, aceptado, rechazado
    'codigo_sunat',             // Código de respuesta SUNAT
    'mensaje_sunat',            // Mensaje de respuesta SUNAT
    'id_empresa',
    'id_usuario',
]

Flujo Paso a Paso

1

Acceder al Formulario

Ruta: /nota-credito/crearComponente: NotaCreditoForm.jsxEl formulario se divide en dos columnas:
  • Izquierda: Búsqueda de comprobante y vista de detalles
  • Derecha: Selección de motivo y configuración de NC
2

Buscar Comprobante a Afectar

Ingreso de Serie y Número

// NotaCreditoForm.jsx línea 88-115
const handleBuscar = async () => {
    const params = new URLSearchParams({
        serie: serie.trim(),  // Ej: F001, B001
        numero: numero.trim() // Ej: 123
    });
    
    const res = await fetch(
        baseUrl(`/api/notas-credito/buscar-venta?${params}`),
        { headers: getAuthHeaders() }
    );
    
    const data = await res.json();
    
    if (data.success && data.venta) {
        setVenta(data.venta);
    } else {
        setErrorBusqueda(data.message || 'Venta no encontrada');
    }
};
Backend endpoint:
// NotaCreditoController.php línea 196-222
public function buscarVenta(Request $request): JsonResponse
{
    $request->validate([
        'serie' => 'required|string|max:4',
        'numero' => 'required|string',
    ]);
    
    $user = $request->user();
    
    $venta = Venta::with(['cliente', 'tipoDocumento', 'productosVentas.producto'])
        ->where('id_empresa', $user->id_empresa)
        ->where('serie', strtoupper($request->serie))
        ->where('numero', (int) $request->numero)
        ->first();
    
    if (!$venta) {
        return response()->json([
            'success' => false,
            'message' => 'Venta no encontrada con esa serie y número.'
        ], 404);
    }
    
    return response()->json([
        'success' => true,
        'venta' => $venta,
    ]);
}
Solo se pueden afectar Facturas y Boletas ya emitidas. Las Notas de Venta no pueden tener Notas de Crédito.
3

Revisar Datos del Comprobante

Una vez encontrada la venta, el sistema muestra:

Información del Cliente

// NotaCreditoForm.jsx línea 342-387
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
    <div>
        <p className="text-xs text-gray-500">Razón Social / Nombre</p>
        <p className="font-medium">{venta.cliente?.datos || 'N/A'}</p>
    </div>
    <div>
        <p className="text-xs text-gray-500">RUC / DNI</p>
        <p className="font-medium">{venta.cliente?.documento || '—'}</p>
    </div>
    <div>
        <p className="text-xs text-gray-500">Dirección</p>
        <p>{venta.cliente?.direccion || '—'}</p>
    </div>
    <div>
        <p className="text-xs text-gray-500">Fecha de Emisión</p>
        <p>{new Date(venta.fecha_emision).toLocaleDateString('es-PE')}</p>
    </div>
</div>

Resumen del Comprobante

// NotaCreditoForm.jsx línea 395-418
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
    <div className="bg-gray-50 rounded-lg p-3 text-center">
        <p className="text-xs text-gray-500">Tipo</p>
        <p className="font-semibold">{tipoDoc}</p>
    </div>
    <div>
        <p className="text-xs text-gray-500">Serie-Número</p>
        <p className="font-mono">{venta.serie}-{venta.numero}</p>
    </div>
    <div>
        <p className="text-xs text-gray-500">Moneda</p>
        <p>{venta.tipo_moneda === 'USD' ? 'Dólares' : 'Soles'}</p>
    </div>
    <div className="bg-green-50 border border-green-200">
        <p className="text-xs text-green-600">Total</p>
        <p className="font-bold text-green-700">
            {monedaSimbolo} {parseFloat(venta.total).toFixed(2)}
        </p>
    </div>
</div>

Tabla de Productos

// NotaCreditoForm.jsx línea 424-507
<Table>
    <TableHeader>
        <TableRow>
            <TableHead>#</TableHead>
            <TableHead>Producto</TableHead>
            <TableHead className="text-center">Cant.</TableHead>
            <TableHead className="text-right">P. Unit.</TableHead>
            <TableHead className="text-right">Subtotal</TableHead>
        </TableRow>
    </TableHeader>
    <TableBody>
        {productos.map((item, idx) => (
            <TableRow key={idx}>
                <TableCell>{String(idx + 1).padStart(2, '0')}</TableCell>
                <TableCell>
                    <p className="font-medium">
                        {item.producto?.nombre || item.descripcion}
                    </p>
                    {item.producto?.codigo && (
                        <p className="text-xs text-gray-400">
                            COD: {item.producto.codigo}
                        </p>
                    )}
                </TableCell>
                <TableCell className="text-center">{item.cantidad}</TableCell>
                <TableCell className="text-right">
                    {monedaSimbolo} {parseFloat(item.precio_unitario).toFixed(2)}
                </TableCell>
                <TableCell className="text-right font-medium">
                    {monedaSimbolo} {(item.cantidad * item.precio_unitario).toFixed(2)}
                </TableCell>
            </TableRow>
        ))}
    </TableBody>
    <TableFooter>
        <TableRow>
            <TableCell colSpan={4}>Subtotal</TableCell>
            <TableCell className="text-right">
                {monedaSimbolo} {parseFloat(venta.subtotal).toFixed(2)}
            </TableCell>
        </TableRow>
        <TableRow>
            <TableCell colSpan={4}>IGV (18%)</TableCell>
            <TableCell className="text-right">
                {monedaSimbolo} {parseFloat(venta.igv).toFixed(2)}
            </TableCell>
        </TableRow>
        <TableRow className="font-bold">
            <TableCell colSpan={4}>Total</TableCell>
            <TableCell className="text-right text-primary-600">
                {monedaSimbolo} {parseFloat(venta.total).toFixed(2)}
            </TableCell>
        </TableRow>
    </TableFooter>
</Table>
La Nota de Crédito se emitirá por el monto total del comprobante. Para NC parciales, requiere desarrollo adicional.
4

Seleccionar Motivo

Select de Motivos

// NotaCreditoForm.jsx línea 529-547
<Select value={motivoId} onValueChange={setMotivoId}>
    <SelectTrigger>
        <SelectValue placeholder="Seleccione un motivo" />
    </SelectTrigger>
    <SelectContent>
        {motivos.map((m) => (
            <SelectItem key={m.id} value={String(m.id)}>
                {m.codigo_sunat} - {m.descripcion}
            </SelectItem>
        ))}
    </SelectContent>
</Select>
Cargar motivos desde API:
// NotaCreditoForm.jsx línea 69-85
useEffect(() => {
    fetchMotivos();
}, []);

const fetchMotivos = async () => {
    const res = await fetch(baseUrl('/api/notas-credito/motivos'), {
        headers: getAuthHeaders(),
    });
    const data = await res.json();
    if (data.success) {
        setMotivos(data.data);
    }
};
Backend:
// NotaCreditoController.php línea 224-232
public function motivos(): JsonResponse
{
    $motivos = MotivoNota::where('tipo', 'NC')
        ->where('estado', true)
        ->get();
    
    return response()->json(['success' => true, 'data' => $motivos]);
}

Descripción Adicional (Opcional)

// NotaCreditoForm.jsx línea 551-563
<Input
    value={descripcion}
    onChange={(e) => setDescripcion(e.target.value)}
    placeholder="Detalle del motivo"
/>
Si no se ingresa descripción, se usa automáticamente la descripción del motivo seleccionado.
5

Crear Nota de Crédito

Endpoint: POST /api/notas-credito

Frontend envía:

// NotaCreditoForm.jsx línea 124-152
const handleSubmit = async () => {
    if (!venta || !motivoId) return;
    setGuardando(true);
    
    const res = await fetch(baseUrl('/api/notas-credito'), {
        method: 'POST',
        headers: getAuthHeaders(),
        body: JSON.stringify({
            id_venta: venta.id_venta,
            motivo_id: parseInt(motivoId),
            descripcion_motivo: descripcion || undefined,
        }),
    });
    
    const data = await res.json();
    
    if (data.success) {
        toast.success('Nota de crédito creada y XML generado');
        window.location.href = baseUrl('/nota-credito');
    } else {
        toast.error(data.message || 'Error al crear nota de crédito');
    }
    
    setGuardando(false);
};

Backend proceso:

// NotaCreditoController.php línea 31-107
public function store(Request $request): JsonResponse
{
    $request->validate([
        'id_venta' => 'required|exists:ventas,id_venta',
        'motivo_id' => 'required|exists:motivo_nota,id',
        'descripcion_motivo' => 'nullable|string|max:255',
    ]);
    
    try {
        return DB::transaction(function () use ($request) {
            // 1. Cargar venta con relaciones
            $venta = Venta::with([
                'empresa', 
                'cliente', 
                'tipoDocumento', 
                'productosVentas'
            ])->findOrFail($request->id_venta);
            
            $empresa = $venta->empresa;
            $motivo = MotivoNota::findOrFail($request->motivo_id);
            
            // 2. Determinar serie de NC según tipo de documento afectado
            $tipDocAfectado = $venta->tipoDocumento->cod_sunat;
            $serieNC = $tipDocAfectado === '01' ? 'FC01' : 'BC01';
            //         Factura (01) → FC01
            //         Boleta (03) → BC01
            
            // 3. Obtener último número de la serie
            $ultimoNumero = NotaCredito::where('serie', $serieNC)
                ->where('id_empresa', $empresa->id_empresa)
                ->max('numero') ?? 0;
            
            // Consultar documentos_empresas como número base configurable
            $numeroBase = DB::table('documentos_empresas')
                ->where('id_empresa', $empresa->id_empresa)
                ->where('serie', $serieNC)
                ->value('numero') ?? 0;
            
            $ultimoNumero = max($ultimoNumero, $numeroBase);
            
            // Sincronizar documentos_empresas
            DB::table('documentos_empresas')
                ->where('id_empresa', $empresa->id_empresa)
                ->where('serie', $serieNC)
                ->update(['numero' => $ultimoNumero + 1]);
            
            // 4. Crear Nota de Crédito
            $nota = NotaCredito::create([
                'id_venta' => $venta->id_venta,
                'motivo_id' => $motivo->id,
                'serie' => $serieNC,
                'numero' => $ultimoNumero + 1,
                'tipo_doc_afectado' => $tipDocAfectado,
                'serie_num_afectado' => $venta->serie . '-' . $venta->numero,
                'descripcion_motivo' => $request->descripcion_motivo ?? $motivo->descripcion,
                'monto_subtotal' => $venta->subtotal,
                'monto_igv' => $venta->igv,
                'monto_total' => $venta->total,
                'moneda' => $venta->tipo_moneda ?? 'PEN',
                'fecha_emision' => now()->toDateString(),
                'estado' => 'pendiente',
                'id_empresa' => $empresa->id_empresa,
                'id_usuario' => $request->user()->id,
            ]);
            
            // 5. Generar XML automáticamente
            $resultado = $this->sunatService->generarNotaCreditoXml($nota);
            
            $nota->load(['venta.cliente', 'motivo']);
            
            return response()->json([
                'success' => true,
                'data' => $nota,
                'xml' => $resultado,
            ], 201);
        });
    } catch (\Exception $e) {
        Log::error('SUNAT - Error al crear nota de crédito', [
            'venta_id' => $request->id_venta,
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
        ]);
        
        return response()->json([
            'success' => false,
            'message' => 'Error al crear nota de crédito: ' . $e->getMessage(),
        ], 500);
    }
}
El XML se genera automáticamente al crear la Nota de Crédito. No requiere paso adicional.
6

Generar XML con Greenter

Servicio: SunatService::generarNotaCreditoXml()
// app/Services/SunatService.php
public function generarNotaCreditoXml(NotaCredito $nota)
{
    $venta = $nota->venta;
    $empresa = $venta->empresa;
    $cliente = $venta->cliente;
    
    // 1. Configurar Greenter
    $see = $this->getSee($empresa);
    
    // 2. Crear objeto Note
    $note = new Note();
    $note->setUblVersion('2.1');
    $note->setTipoDoc('07');  // Nota de Crédito
    $note->setSerie($nota->serie);
    $note->setCorrelativo($nota->numero);
    $note->setFechaEmision($this->fechaParaGreenter($nota->fecha_emision));
    
    // 3. Datos del emisor (empresa)
    $address = new Address();
    $address->setDireccion($empresa->direccion);
    $address->setUbigueo($empresa->ubigeo ?? '150101');
    
    $company = new Company();
    $company->setRuc($empresa->ruc);
    $company->setRazonSocial($empresa->razon_social);
    $company->setNombreComercial($empresa->comercial);
    $company->setAddress($address);
    $note->setCompany($company);
    
    // 4. Datos del cliente
    $client = new Client();
    $client->setTipoDoc($cliente->tipo_doc);
    $client->setNumDoc($cliente->documento);
    $client->setRznSocial($cliente->datos);
    $note->setClient($client);
    
    // 5. Documento afectado
    $note->setTipDocAfectado($nota->tipo_doc_afectado);
    $note->setNumDocfectado($nota->serie_num_afectado);
    
    // 6. Motivo
    $note->setCodMotivo($nota->motivo->codigo_sunat);  // '01', '02', etc.
    $note->setDesMotivo($nota->descripcion_motivo);
    
    // 7. Moneda
    $note->setTipoMoneda($nota->moneda);
    
    // 8. Items (productos de la venta original)
    foreach ($venta->productosVentas as $detalle) {
        $item = new SaleDetail();
        $item->setCodProducto($detalle->codigo_producto ?: 'PROD');
        $item->setUnidad($detalle->unidad_medida ?: 'NIU');
        $item->setCantidad($detalle->cantidad);
        $item->setDescripcion($detalle->descripcion);
        $item->setMtoBaseIgv($detalle->subtotal);
        $item->setPorcentajeIgv(18.00);
        $item->setIgv($detalle->igv);
        $item->setTipAfeIgv($detalle->tipo_afectacion_igv ?: '10');
        $item->setTotalImpuestos($detalle->igv);
        $item->setMtoValorVenta($detalle->subtotal);
        $item->setMtoValorUnitario($detalle->precio_unitario);
        $item->setMtoPrecioUnitario($detalle->precio_unitario * 1.18);
        $note->addDetail($item);
    }
    
    // 9. Totales
    $note->setMtoOperGravadas($nota->monto_subtotal);
    $note->setMtoIGV($nota->monto_igv);
    $note->setTotalImpuestos($nota->monto_igv);
    $note->setMtoImpVenta($nota->monto_total);
    
    // 10. Leyenda (monto en letras)
    $legend = new Legend();
    $legend->setCode('1000');
    $legend->setValue($this->numeroALetras($nota->monto_total, $nota->moneda));
    $note->setLegends([$legend]);
    
    // 11. Generar XML
    $xml = $see->getXmlSigned($note);
    
    // 12. Guardar en storage
    $nombreXml = $empresa->ruc . '-07-' . $nota->serie . '-' . $nota->numero;
    $rutaXml = "sunat/xml/{$empresa->ruc}/{$nombreXml}.xml";
    Storage::put($rutaXml, $xml);
    
    // 13. Actualizar nota con datos del XML
    $nota->update([
        'nombre_xml' => $nombreXml,
        'xml_url' => $rutaXml,
    ]);
    
    return [
        'success' => true,
        'nombre_xml' => $nombreXml,
        'ruta' => $rutaXml,
    ];
}
El XML se firma con el certificado digital de la empresa ({ruc}-cert.pem) antes de guardar.
7

Enviar a SUNAT

Desde la lista de Notas de Crédito, hacer click en “Enviar SUNAT”.Endpoint: POST /api/notas-credito/{id}/enviar
// NotaCreditoController.php línea 117-143
public function enviar(int $id): JsonResponse
{
    $nota = NotaCredito::with(['venta.empresa'])->findOrFail($id);
    
    if (!$nota->nombre_xml) {
        return response()->json([
            'success' => false,
            'message' => 'Primero debe generar el XML.',
        ], 422);
    }
    
    try {
        $resultado = $this->sunatService->enviarNotaCredito($nota);
        return response()->json($resultado);
    } catch (\Exception $e) {
        Log::error('SUNAT - Error al enviar nota de crédito', [
            'nota_id' => $id,
            'serie' => $nota->serie . '-' . $nota->numero,
            'error' => $e->getMessage(),
        ]);
        
        return response()->json([
            'success' => false,
            'message' => 'Error al enviar NC a SUNAT: ' . $e->getMessage(),
        ], 500);
    }
}

Proceso de envío:

// SunatService.php
public function enviarNotaCredito(NotaCredito $nota)
{
    $empresa = $nota->venta->empresa;
    
    // 1. Leer XML generado
    $xmlPath = storage_path("app/{$nota->xml_url}");
    $xml = file_get_contents($xmlPath);
    
    // 2. Configurar Greenter
    $see = $this->getSee($empresa);
    
    // 3. Enviar a SUNAT (SOAP)
    $result = $see->sendXml('07', $nota->serie, $nota->numero, $xml);
    
    if (!$result->isSuccess()) {
        // Error de conexión o rechazo
        $error = $result->getError();
        
        Log::error('SUNAT - Error al enviar NC', [
            'codigo' => $error->getCode(),
            'mensaje' => $error->getMessage(),
        ]);
        
        return [
            'success' => false,
            'message' => "Error SUNAT: {$error->getCode()} - {$error->getMessage()}",
        ];
    }
    
    // 4. Procesar CDR (Constancia de Recepción)
    $cdr = $result->getCdrResponse();
    $cdrZip = $result->getCdrZip();
    
    // 5. Guardar CDR
    $nombreCdr = "R-{$nota->nombre_xml}.zip";
    $rutaCdr = "sunat/cdr/{$empresa->ruc}/{$nombreCdr}";
    Storage::put($rutaCdr, $cdrZip);
    
    // 6. Actualizar estado de la nota
    $codigoSunat = $cdr->getCode();
    $mensajeSunat = $cdr->getDescription();
    
    $estado = 'aceptado';
    if ($codigoSunat >= 4000 && $codigoSunat < 5000) {
        $estado = 'aceptado_obs';  // Aceptado con observaciones
    } elseif ($codigoSunat >= 2000 && $codigoSunat < 4000) {
        $estado = 'rechazado';
    }
    
    $nota->update([
        'cdr_url' => $rutaCdr,
        'estado' => $estado,
        'codigo_sunat' => $codigoSunat,
        'mensaje_sunat' => $mensajeSunat,
    ]);
    
    return [
        'success' => true,
        'message' => "NC enviada exitosamente. Código SUNAT: {$codigoSunat}",
        'codigo' => $codigoSunat,
        'descripcion' => $mensajeSunat,
    ];
}

Códigos de respuesta SUNAT:

CódigoEstadoDescripción
0AceptadoComprobante válido
4XXXAceptado con observacionesVálido pero tiene advertencias
2XXXRechazadoComprobante inválido
Si SUNAT rechaza la NC (código 2XXX), revisar el mensaje de error y corregir antes de reenviar.
8

Descargar Documentos

Descargar XML

Endpoint: GET /api/notas-credito/xml/{nombre_xml}.xml
// NotaCreditoController.php línea 168-194
public function xml(string $nombre)
{
    $nombreXml = preg_replace('/\.xml$/i', '', $nombre);
    
    $nota = NotaCredito::where('nombre_xml', $nombreXml)->first();
    
    if (!$nota || !$nota->xml_url) {
        return response()->json([
            'success' => false,
            'message' => 'XML no encontrado.'
        ], 404);
    }
    
    $path = storage_path("app/{$nota->xml_url}");
    
    if (!file_exists($path)) {
        return response()->json([
            'success' => false,
            'message' => 'Archivo XML no encontrado en el servidor.'
        ], 404);
    }
    
    return response()->file($path, [
        'Content-Type' => 'application/xml',
        'Content-Disposition' => "inline; filename=\"{$nombreXml}.xml\"",
    ]);
}

Descargar CDR

Endpoint: GET /api/notas-credito/{id}/cdr
// NotaCreditoController.php línea 145-166
public function cdr(int $id)
{
    $nota = NotaCredito::findOrFail($id);
    
    if (!$nota->cdr_url) {
        return response()->json([
            'success' => false,
            'message' => 'CDR no disponible.'
        ], 404);
    }
    
    $path = storage_path("app/{$nota->cdr_url}");
    
    if (!file_exists($path)) {
        return response()->json([
            'success' => false,
            'message' => 'Archivo CDR no encontrado en el servidor.'
        ], 404);
    }
    
    return response()->download($path, "R-{$nota->nombre_xml}.zip");
}

Consideraciones Importantes

Limitaciones actuales

Estado actual: El sistema emite NC por el monto total del comprobante.Para NC parciales:
  • Requiere desarrollo adicional
  • Permitir selección de items específicos
  • Ingresar cantidades parciales
  • Recalcular totales
Comportamiento: La NC no devuelve productos al inventario.Solución manual:
  1. Ir a módulo Almacén
  2. Crear ajuste de entrada por devolución
  3. Referenciar NC como motivo
Desarrollo futuro: Agregar opción “Devolver al inventario” al crear NC.
Comportamiento: La NC no afecta automáticamente las cuotas pendientes.Proceso manual:
  1. Ir a Cuentas por Cobrar
  2. Registrar pago con referencia a NC
  3. O anular las cuotas manualmente
Recomendación: Documentar la NC en observaciones de la cuota.

Errores Comunes

Causa: Serie o número incorrecto, o venta no existe en la base de datos.Solución:
  • Verificar serie y número en lista de ventas
  • Revisar que sea Factura o Boleta (no Nota de Venta)
  • Confirmar que la venta pertenece a la empresa actual
Causa: El comprobante original no fue enviado a SUNAT o fue rechazado.Solución:
  1. Verificar que la venta tenga estado_sunat = '1' (Aceptado)
  2. Si no, enviar primero la venta a SUNAT
  3. Esperar confirmación de CDR
  4. Luego crear la NC
Causa: La fecha de la NC es anterior a la fecha del comprobante afectado.Solución:
  • El sistema usa now() automáticamente
  • Si ocurre este error, revisar fecha_emision de la venta
  • Ajustar manualmente si es necesario
Causa: Certificado inválido o credenciales SOL incorrectas.Solución:
  1. Verificar certificado en storage/app/sunat/certificados/{ruc}-cert.pem
  2. Revisar credenciales en Configuración → Empresa:
    • Usuario SOL
    • Clave SOL
    • Modo (beta/producción)
  3. Verificar logs en storage/logs/laravel.log

Referencias Técnicas

Controlador: app/Http/Controllers/NotaCreditoController.php Servicio: app/Services/SunatService.php Modelos:
  • app/Models/NotaCredito.php
  • app/Models/MotivoNota.php
Frontend:
  • resources/js/components/NotaCredito/NotaCreditoForm.jsx
  • resources/js/components/NotaCredito/page.jsx
Rutas API:
GET    /api/notas-credito                         // Listar
POST   /api/notas-credito                         // Crear
GET    /api/notas-credito/{id}                    // Ver detalle
POST   /api/notas-credito/{id}/enviar             // Enviar a SUNAT
GET    /api/notas-credito/{id}/cdr                // Descargar CDR
GET    /api/notas-credito/xml/{nombre}.xml        // Descargar XML
GET    /api/notas-credito/buscar-venta            // Buscar venta
GET    /api/notas-credito/motivos                 // Listar motivos
Documentación SUNAT:

Build docs developers (and LLMs) love