Skip to main content

Notas de Crédito y Débito

Las notas de crédito y débito son documentos electrónicos que modifican comprobantes previamente emitidos, permitiendo anulaciones, descuentos, devoluciones o aumentos de valor.

Tipos de Notas

Nota de Crédito (07)

Reduce el monto de una factura o boleta por anulación, descuento o devolución

Nota de Débito (08)

Aumenta el monto de una factura o boleta por intereses o servicios adicionales

Nota de Crédito

Motivos de Emisión

SUNAT define motivos específicos para emitir notas de crédito:
Anula completamente la venta. El cliente devuelve todos los productos.
Factura emitida con RUC incorrecto.
Error en la descripción de productos o servicios.
Descuento aplicado después de emitida la factura.
Descuento en productos específicos.
Cliente devuelve todos los productos.
Cliente devuelve algunos productos.
Producto entregado sin costo adicional.
Reducción del valor por acuerdo comercial.
Modificación de condiciones de pago.

Flujo de Trabajo

1

Buscar Venta Original

Ingresa la serie y número del comprobante a modificar:
// Búsqueda por serie-número
const venta = await buscarVenta({ serie: 'F001', numero: 125 });
2

Seleccionar Motivo

Elige el motivo de la nota de crédito desde el catálogo de SUNAT:
// NotaCreditoController.php:46
$motivo = MotivoNota::findOrFail($request->motivo_id);
$tipDocAfectado = $venta->tipoDocumento->cod_sunat; // '01' o '03'
3

Determinar Serie

El sistema asigna la serie automáticamente según el documento afectado:
  • Factura (01) → Serie FC01, FC02…
  • Boleta (03) → Serie BC01, BC02…
// NotaCreditoController.php:48-49
$serieNC = $tipDocAfectado === '01' ? 'FC01' : 'BC01';
4

Generar Número Correlativo

Se obtiene el siguiente número para la serie:
// NotaCreditoController.php:50-66
$ultimoNumero = NotaCredito::where('serie', $serieNC)
    ->where('id_empresa', $empresa->id_empresa)
    ->max('numero') ?? 0;

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

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

Crear Nota de Crédito

Se crea el documento con los datos de la venta original:
// NotaCreditoController.php:68-84
$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,
    'monto_subtotal' => $venta->subtotal,
    'monto_igv' => $venta->igv,
    'monto_total' => $venta->total,
    'moneda' => $venta->tipo_moneda ?? 'PEN',
    'estado' => 'pendiente'
]);
6

Generar XML

El sistema delega al SunatService para generar el XML:
// NotaCreditoController.php:86
$resultado = $this->sunatService->generarNotaCreditoXml($nota);
7

Enviar a SUNAT

Una vez generado el XML, se envía usando el endpoint /enviar:
// NotaCreditoController.php:117-143
public function enviar(int $id): JsonResponse
{
    $nota = NotaCredito::with(['venta.empresa'])->findOrFail($id);
    
    if (!$nota->nombre_xml) {
        return response()->json([
            'message' => 'Primero debe generar el XML'
        ], 422);
    }
    
    $resultado = $this->sunatService->enviarNotaCredito($nota);
    return response()->json($resultado);
}

Implementación Backend

// app/Http/Controllers/NotaCreditoController.php

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',
    ]);

    return DB::transaction(function () use ($request) {
        $venta = Venta::with([
            'empresa', 'cliente', 'tipoDocumento', 'productosVentas'
        ])->findOrFail($request->id_venta);

        $empresa = $venta->empresa;
        $motivo = MotivoNota::findOrFail($request->motivo_id);

        // Determinar tipo de documento afectado y serie
        $tipDocAfectado = $venta->tipoDocumento->cod_sunat;
        $serieNC = $tipDocAfectado === '01' ? 'FC01' : 'BC01';

        // Obtener próximo número
        $ultimoNumero = NotaCredito::where('serie', $serieNC)
            ->where('id_empresa', $empresa->id_empresa)
            ->max('numero') ?? 0;

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

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

        // 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,
        ]);

        // Generar XML usando SunatService
        $resultado = $this->sunatService->generarNotaCreditoXml($nota);

        $nota->load(['venta.cliente', 'motivo']);

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

Búsqueda de Venta

El endpoint de búsqueda permite encontrar la venta por serie-número:
// NotaCreditoController.php: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'
        ], 404);
    }

    return response()->json([
        'success' => true,
        'venta' => $venta,
    ]);
}

Nota de Débito

Motivos de Emisión

La nota de débito se usa principalmente para:
  • Intereses por mora: Cuando el cliente paga fuera de plazo
  • Penalidades: Cargos adicionales según contrato
  • Servicios adicionales: Servicios prestados después de la factura original
  • Aumento en el valor: Correcciones que incrementan el monto
La implementación de notas de débito es similar a las de crédito, pero con motivos diferentes y series NDxxx.

Integración con SUNAT

Generación de XML

El SunatService utiliza la biblioteca Greenter para generar el XML:
// app/Services/SunatService.php (método generarNotaCreditoXml)

public function generarNotaCreditoXml(NotaCredito $nota)
{
    $venta = $nota->venta;
    $empresa = $venta->empresa;

    // Construir objeto Greenter
    $creditNote = new Note();
    $creditNote
        ->setUblVersion('2.1')
        ->setTipoDoc('07')  // Nota de Crédito
        ->setSerie($nota->serie)
        ->setCorrelativo($nota->numero)
        ->setFechaEmision(new \DateTime($nota->fecha_emision))
        ->setTipDocAfectado($nota->tipo_doc_afectado)
        ->setNumDocfectado($nota->serie_num_afectado)
        ->setCodMotivo($nota->motivo->codigo)
        ->setDesMotivo($nota->descripcion_motivo)
        ->setTipoMoneda($nota->moneda)
        ->setCompany($this->buildCompany($empresa))
        ->setClient($this->buildClient($venta->cliente))
        ->setMtoOperGravadas($nota->monto_subtotal)
        ->setMtoIGV($nota->monto_igv)
        ->setTotalImpuestos($nota->monto_igv)
        ->setMtoImpVenta($nota->monto_total);

    // Añadir ítems de la venta original
    $items = [];
    foreach ($venta->productosVentas as $detalle) {
        $item = new SaleDetail();
        $item->setCodProducto($detalle->codigo_producto)
            ->setUnidad($detalle->unidad_medida)
            ->setCantidad($detalle->cantidad)
            ->setDescripcion($detalle->descripcion)
            ->setMtoBaseIgv($detalle->subtotal)
            ->setPorcentajeIgv(18.00)
            ->setIgv($detalle->igv)
            ->setTipAfeIgv($detalle->tipo_afectacion_igv)
            ->setTotalImpuestos($detalle->igv)
            ->setMtoValorVenta($detalle->subtotal)
            ->setMtoValorUnitario($detalle->precio_unitario / 1.18)
            ->setMtoPrecioUnitario($detalle->precio_unitario);
        
        $items[] = $item;
    }
    $creditNote->setDetails($items);

    // Generar XML
    $xml = $this->see->getXmlSigned($creditNote);
    
    // Guardar archivo XML
    $nombreXml = $this->buildFileName($empresa->ruc, '07', $nota->serie, $nota->numero);
    $rutaXml = "sunat/xml/{$empresa->ruc}/{$nombreXml}.xml";
    Storage::put($rutaXml, $xml);
    
    $nota->update([
        'nombre_xml' => $nombreXml,
        'xml_url' => $rutaXml,
    ]);
    
    return ['success' => true, 'xml' => $nombreXml];
}

Envío SOAP

Las notas de crédito se envían vía SOAP igual que facturas:
public function enviarNotaCredito(NotaCredito $nota)
{
    $rutaXml = storage_path("app/{$nota->xml_url}");
    
    // Leer XML firmado
    $xml = file_get_contents($rutaXml);
    
    // Enviar a SUNAT
    $result = $this->see->send($xml);
    
    if ($result->isSuccess()) {
        // Guardar CDR
        $cdrZip = $result->getCdrZip();
        $nombreCdr = "R-{$nota->nombre_xml}";
        $rutaCdr = "sunat/cdr/{$nota->venta->empresa->ruc}/{$nombreCdr}.zip";
        Storage::put($rutaCdr, $cdrZip);
        
        $nota->update([
            'estado' => 'aceptado',
            'codigo_sunat' => $result->getCdrResponse()->getCode(),
            'mensaje_sunat' => $result->getCdrResponse()->getDescription(),
            'cdr_url' => $rutaCdr,
        ]);
        
        return ['success' => true, 'message' => 'Nota enviada exitosamente'];
    } else {
        $nota->update([
            'estado' => 'rechazado',
            'codigo_sunat' => $result->getError()->getCode(),
            'mensaje_sunat' => $result->getError()->getMessage(),
        ]);
        
        return ['success' => false, 'message' => $result->getError()->getMessage()];
    }
}

Descarga de Documentos

El sistema permite descargar el XML y el CDR:

Descargar CDR

// NotaCreditoController.php: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'
        ], 404);
    }

    return response()->download($path, "R-{$nota->nombre_xml}.zip");
}

Descargar XML

// NotaCreditoController.php: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'
        ], 404);
    }

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

Catálogo de Motivos

Los motivos están en la tabla motivo_nota:
// NotaCreditoController.php:224-231
public function motivos(): JsonResponse
{
    $motivos = MotivoNota::where('tipo', 'NC')
        ->where('estado', true)
        ->get();

    return response()->json(['success' => true, 'data' => $motivos]);
}

Tablas de Base de Datos

TablaDescripciónCampos Clave
notas_creditoNotas de crédito emitidasid, id_venta, serie, numero, motivo_id, estado, estado_sunat
notas_debitoNotas de débito emitidasSimilar a notas_credito
motivo_notaCatálogo de motivos SUNATid, codigo, descripcion, tipo (NC/ND)
Cada nota mantiene referencia a la venta original mediante id_venta y guarda el identificador del documento afectado en formato serie-numero.

Endpoints API

Listar NC

GET /api/notas-credito

Crear NC

POST /api/notas-credito

Enviar NC

POST /api/notas-credito/:id/enviar

Buscar Venta

POST /api/notas-credito/buscar-venta

Buenas Prácticas

Importante: Una nota de crédito NO anula automáticamente la venta en el sistema. Solo modifica el comprobante ante SUNAT. Para anular la venta internamente, usa la opción “Anular Venta” en el módulo de facturación.
Para anulaciones totales, considera usar Comunicación de Baja en lugar de nota de crédito si la venta fue enviada el mismo día.

Próximos Pasos

Comunicación de Baja

Aprende a anular documentos vía SUNAT

Guía: Emitir NC

Tutorial paso a paso

Consultar CDR

Verifica el estado en SUNAT

Tipos de Documentos

Catálogo completo de documentos SUNAT

Build docs developers (and LLMs) love