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:
01 - Anulación de la operación
Anula completamente la venta. El cliente devuelve todos los productos.
02 - Anulación por error en el RUC
Factura emitida con RUC incorrecto.
03 - Corrección por error en la descripción
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.
09 - Disminución en el valor
Reducción del valor por acuerdo comercial.
13 - Ajustes - montos y/o fechas de pago
Modificación de condiciones de pago.
Flujo de Trabajo
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 });
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'
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' ;
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 );
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'
]);
Generar XML
El sistema delega al SunatService para generar el XML: // NotaCreditoController.php:86
$resultado = $this -> sunatService -> generarNotaCreditoXml ( $nota );
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
Tabla Descripción Campos Clave notas_creditoNotas de crédito emitidas id, id_venta, serie, numero, motivo_id, estado, estado_sunatnotas_debitoNotas de débito emitidas Similar a notas_credito motivo_notaCatálogo de motivos SUNAT id, 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