Skip to main content

Descripción General

Las notas de crédito y débito son documentos electrónicos que modifican comprobantes de pago ya emitidos, cumpliendo con la normativa SUNAT.

Notas de Crédito

Se emiten para:
  • Anulaciones de operaciones
  • Descuentos otorgados después de la emisión
  • Devoluciones de mercadería
  • Corrección de errores en el monto

Notas de Débito

Se emiten para:
  • Intereses por mora
  • Aumento en el valor de la operación
  • Gastos adicionales no contemplados

Notas de Crédito

Listar Notas de Crédito

public function index(Request $request): JsonResponse
{
    $idEmpresa = $request->user()->id_empresa;

    $notas = NotaCredito::with(['venta.cliente', 'motivo'])
        ->where('id_empresa', $idEmpresa)
        ->orderBy('id', 'desc')
        ->paginate(15);

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

Crear Nota de Crédito

Crea una nota de crédito asociada a una venta existente.
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 serie según tipo de documento afectado
        $tipDocAfectado = $venta->tipoDocumento->cod_sunat;
        $serieNC = $tipDocAfectado === '01' ? 'FC01' : 'BC01';

        $ultimoNumero = NotaCredito::where('serie', $serieNC)
            ->where('id_empresa', $empresa->id_empresa)
            ->max('numero') ?? 0;

        $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
        $resultado = $this->sunatService->generarNotaCreditoXml($nota);

        return response()->json([
            'success' => true,
            'data' => $nota,
            'xml' => $resultado,
        ], 201);
    });
}
Series de Notas de Crédito
  • FC01: Para notas de crédito que afectan Facturas
  • BC01: Para notas de crédito que afectan Boletas

Enviar a SUNAT

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,
            'error' => $e->getMessage(),
        ]);
        return response()->json([
            'success' => false,
            'message' => 'Error al enviar NC a SUNAT: ' . $e->getMessage(),
        ], 500);
    }
}

Buscar Venta para Nota de Crédito

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]);
}

Obtener Motivos de Nota de Crédito

public function motivos(): JsonResponse
{
    $motivos = MotivoNota::where('tipo', 'NC')
        ->where('estado', true)
        ->get();

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

Notas de Débito

Crear Nota de Débito

Las notas de débito requieren especificar el monto a aumentar.
public function store(Request $request): JsonResponse
{
    $request->validate([
        'id_venta' => 'required|exists:ventas,id_venta',
        'motivo_id' => 'required|exists:motivo_nota,id',
        'monto_total' => 'required|numeric|min:0.01',
        'descripcion_motivo' => 'nullable|string|max:255',
    ]);

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

        $empresa = $venta->empresa;
        $motivo = MotivoNota::findOrFail($request->motivo_id);
        $igvRate = (float) ($empresa->igv ?? config('sunat.igv'));

        $tipDocAfectado = $venta->tipoDocumento->cod_sunat;
        $serieND = $tipDocAfectado === '01' ? 'FC01' : 'BC01';

        $ultimoNumero = NotaDebito::where('serie', $serieND)
            ->where('id_empresa', $empresa->id_empresa)
            ->max('numero') ?? 0;

        // Calcular subtotal e IGV del monto total
        $total = (float) $request->monto_total;
        $subtotal = round($total / ($igvRate + 1), 2);
        $igv = round($total - $subtotal, 2);

        $nota = NotaDebito::create([
            'id_venta' => $venta->id_venta,
            'motivo_id' => $motivo->id,
            'serie' => $serieND,
            'numero' => $ultimoNumero + 1,
            'tipo_doc_afectado' => $tipDocAfectado,
            'serie_num_afectado' => $venta->serie . '-' . $venta->numero,
            'descripcion_motivo' => $request->descripcion_motivo ?? $motivo->descripcion,
            'monto_subtotal' => $subtotal,
            'monto_igv' => $igv,
            'monto_total' => $total,
            'moneda' => $venta->tipo_moneda ?? 'PEN',
            'fecha_emision' => now()->toDateString(),
            'estado' => 'pendiente',
            'id_empresa' => $empresa->id_empresa,
        ]);

        $resultado = $this->sunatService->generarNotaDebitoXml($nota);

        return response()->json([
            'success' => true,
            'data' => $nota,
            'xml' => $resultado,
        ], 201);
    });
}
El monto ingresado en monto_total incluye IGV. El sistema calcula automáticamente el subtotal y el IGV basándose en la tasa de IGV configurada en la empresa.

Enviar Nota de Débito a SUNAT

public function enviar(int $id): JsonResponse
{
    $nota = NotaDebito::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->enviarNotaDebito($nota);
        return response()->json($resultado);
    } catch (\Exception $e) {
        Log::error('SUNAT - Error al enviar nota de débito', [
            'nota_id' => $id,
            'error' => $e->getMessage(),
        ]);
        return response()->json([
            'success' => false,
            'message' => 'Error al enviar ND a SUNAT: ' . $e->getMessage(),
        ], 500);
    }
}

Modelos de Datos

Nota de Crédito Model

protected $table = 'nota_credito';

protected $fillable = [
    'id_venta',
    'motivo_id',
    'serie',
    'numero',
    'tipo_doc_afectado',
    'serie_num_afectado',
    'descripcion_motivo',
    'monto_subtotal',
    'monto_igv',
    'monto_total',
    'moneda',
    'fecha_emision',
    'estado',
    'xml_url',
    'cdr_url',
    'nombre_xml',
    'id_empresa',
    'id_usuario',
];

protected $casts = [
    'fecha_emision' => 'date',
    'monto_subtotal' => 'decimal:2',
    'monto_igv' => 'decimal:2',
    'monto_total' => 'decimal:2',
];

Nota de Débito Model

protected $table = 'nota_debito';

protected $fillable = [
    'id_venta',
    'motivo_id',
    'serie',
    'numero',
    'tipo_doc_afectado',
    'serie_num_afectado',
    'descripcion_motivo',
    'monto_subtotal',
    'monto_igv',
    'monto_total',
    'moneda',
    'fecha_emision',
    'estado',
    'xml_url',
    'cdr_url',
    'nombre_xml',
    'id_empresa',
    'id_usuario',
];

protected $casts = [
    'fecha_emision' => 'date',
    'monto_subtotal' => 'decimal:2',
    'monto_igv' => 'decimal:2',
    'monto_total' => 'decimal:2',
];

Estados de Notas

Pendiente

La nota fue creada pero aún no se envió a SUNAT

Aceptada

SUNAT aceptó la nota electrónica

Rechazada

SUNAT rechazó la nota por errores

Motivos Comunes

Notas de Crédito (Catálogo 09 SUNAT)

Se utiliza cuando se anula completamente la venta original.
Cuando se emitió el comprobante con un RUC incorrecto.
Para corregir errores en la descripción de productos/servicios.
Cuando se otorga un descuento después de emitido el comprobante.
Descuento aplicado a productos específicos.
Devolución completa de la mercadería.
Devolución parcial de algunos productos.
Cuando se otorga una bonificación posterior.
Reducción del valor de la operación.

Notas de Débito (Catálogo 10 SUNAT)

Cobro de intereses por pago fuera de plazo.
Incremento en el valor de la operación original.
Aplicación de penalidades contractuales.

Descarga de Archivos

Descargar CDR

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");
}

Descargar XML

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\"",
    ]);
}
El CDR (Constancia de Recepción) es el comprobante que SUNAT envía confirmando que el documento fue aceptado.

Build docs developers (and LLMs) love