Skip to main content

Introducción

El flujo de venta en Santo Domingo Facturación Electrónica comprende desde la cotización inicial hasta la emisión del comprobante electrónico y la gestión de cobros. Este proceso se integra completamente con SUNAT para la emisión de facturas y boletas electrónicas válidas.
El sistema permite emitir Boletas (B001), Facturas (F001) y Notas de Venta (NV01). Cada tipo de documento tiene reglas específicas de validación y afectación de stock.

Tipos de Documentos

Boleta de Venta (id_tido=1)

  • Serie: B001
  • Cliente: Requiere DNI (8 dígitos) o CE. NO acepta RUC (11 dígitos)
  • Stock: Descuenta del Almacén 1 (Facturación)
  • SUNAT: Requiere Resumen Diario posterior

Factura (id_tido=2)

  • Serie: F001
  • Cliente: REQUIERE RUC (11 dígitos obligatorio)
  • Stock: Descuenta del Almacén 1 (Facturación)
  • SUNAT: Envío sincrónico con CDR inmediato

Nota de Venta (id_tido=6)

  • Serie: NV01
  • Cliente: Opcional
  • Stock: Descuenta del Almacén 2 (Kardex Real)
  • SUNAT: NO se envía a SUNAT (documento interno)
  • Conversión: Puede convertirse a Factura/Boleta posteriormente

Flujo Completo Paso a Paso

1

Crear Cotización (Opcional)

Iniciar desde /cotizaciones/crearDatos requeridos:
  • Cliente (búsqueda por RUC/DNI o ingreso manual)
  • Productos con cantidades y precios
  • Moneda (PEN/USD)
  • Aplicar IGV (checkbox)
  • Tipo de pago: Contado (1) o Crédito (2)
Backend: CotizacionController@store
  • Genera número correlativo automático (COT-000001)
  • Calcula totales: si aplicar_igv=true, entonces subtotal = total / 1.18
  • Guarda en tabla cotizaciones con estado pendiente
Características:
  • Soporta cuotas de pago programadas
  • Permite precio especial por producto
  • Puede tener descuento general en %
  • No afecta stock hasta conversión
2

Convertir Cotización a Venta

Desde la lista de cotizaciones, hacer clic en “Convertir a Venta”URL: /ventas/crear?tipo=factura&cotizacion_id=123Proceso automático:
// VentaForm.jsx línea 78-149
- Carga datos del cliente desde cotización
- Importa todos los productos con cantidades
- Mantiene moneda y configuración de IGV
- Pre-selecciona tipo de documento según cliente:
  * RUCFactura (F001)
  * DNI/CEBoleta (B001)
Backend actualiza cotización:
// VentasController.php línea 337-341
if (!empty($validated['cotizacion_id'])) {
    Cotizacion::where('id', $cotizacionId)
        ->update(['estado' => 'aprobada']);
}
3

Crear Venta Directa

Acceder a /ventas/crear o desde menú “Nueva Venta”

Selección de Cliente

Componente: ClienteAutocomplete.jsx
  • Búsqueda por RUC/DNI con consulta API Perú
  • Autocompletado desde base de datos local
  • Creación automática si no existe
Validación según tipo de documento:
// VentasController.php línea 136-148
if ($validated['id_tido'] == 2 && strlen($documento) !== 11) {
    return 'Para FACTURA se requiere RUC (11 dígitos)';
}
if ($validated['id_tido'] == 1 && strlen($documento) === 11) {
    return 'Para BOLETA use DNI. Para RUC emita Factura';
}

Agregar Productos

Opciones disponibles:
  1. Búsqueda individual: Autocomplete con selector de precio (precio/costo/precio2/precio3)
  2. Búsqueda múltiple: Selección masiva de productos desde modal
  3. Modo libre: Ingresar descripción sin producto del catálogo
Productos libres:
// VentasController.php línea 232-261
if (!$idProducto && $esProductoLibre) {
    // Busca o crea producto con código LIB-0001
    $productoLibre = Producto::where('nombre', $nombreLibre)
        ->where('id_empresa', $idEmpresa)
        ->first();
    
    if (!$productoLibre) {
        // Genera código automático LIB-XXXX
        $codigoAuto = 'LIB-' . str_pad($ultimoCodigo + 1, 4, '0', STR_PAD_LEFT);
        $productoLibre = Producto::create([...]);
    }
}

Configurar Almacén

Lógica automática:
// VentaForm.jsx línea 218-235
if (formData.id_tido === '6') {
    // Nota de Venta → Almacén 2 (Kardex Real)
    nuevoAlmacen = '2';
} else if (formData.id_tido === '1' || formData.id_tido === '2') {
    // Factura/Boleta → Almacén 1 (SUNAT)
    nuevoAlmacen = '1';
}
4

Configurar Forma de Pago

Tipo de Pago

Contado (id_tipo_pago=1):
  • Pago inmediato al emitir
  • No genera cuentas por cobrar
  • FormaPagoContado en XML SUNAT
Crédito (id_tipo_pago=2):
  • Genera cuotas en tabla dias_ventas
  • FormaPagoCredito con fecha de vencimiento en XML
  • Aparece en módulo Cuentas por Cobrar

Método de Pago

Componente: MetodoPago.jsx
  • Efectivo, Tarjeta, Transferencia, Yape, Plin
  • Permite subir voucher de pago
  • Guarda en tabla ventas_pagos:
// VentasController.php línea 316-334
if ($request->has('pago_id_tipo_pago')) {
    $voucherPath = null;
    if ($request->hasFile('pago_voucher')) {
        $filename = 'voucher_' . $venta->id_venta . '_' . time();
        $voucherPath = $file->storeAs('vouchers', $filename, 'public');
    }
    
    VentaPago::create([
        'id_venta' => $venta->id_venta,
        'id_tipo_pago' => $request->pago_id_tipo_pago,
        'monto' => $venta->total,
        'voucher' => $voucherPath,
    ]);
}

Cuotas de Crédito

Modal: PaymentSchedule.jsx
  • Define monto inicial y cuotas
  • Valida que suma = total venta
  • Guarda en dias_ventas con estado ‘P’ (Pendiente)
5

Guardar Venta

Endpoint: POST /api/ventas

Proceso en backend:

// VentasController.php línea 150-359
DB::transaction(function () use ($validated, $user) {
    // 1. Resolver o crear cliente
    $clienteModel = Cliente::where('documento', $docCliente)
        ->firstOrCreate([...]);
    
    // 2. Calcular próximo número
    $ultimaVenta = Venta::where('serie', $serie)->max('numero');
    $numeroBase = DB::table('documentos_empresas')
        ->where('serie', $serie)
        ->value('numero');
    $proximoNumero = max($ultimaVenta, $numeroBase) + 1;
    
    // 3. Crear venta
    $venta = Venta::create([
        'serie' => $serie,
        'numero' => $proximoNumero,
        'total' => $total,
        'afecta_stock' => $id_tido == 6 ? false : true,
        'estado' => '1', // Activo
        'estado_sunat' => '0', // Pendiente
    ]);
    
    // 4. Guardar productos y descontar stock
    foreach ($validated['productos'] as $producto) {
        ProductoVenta::create([...]);
        
        // Descontar stock si afecta
        if ($afectaStock && !$esProductoLibre) {
            $productoModel->decrement('cantidad', $cantidad);
            
            // Registrar movimiento
            MovimientoStock::create([
                'tipo_movimiento' => 'salida',
                'tipo_documento' => 'venta',
                'id_documento' => $venta->id_venta,
                'documento_referencia' => $venta->serie . '-' . $numero,
            ]);
        }
    }
    
    // 5. Actualizar estado de cotización origen
    if ($cotizacion_id) {
        Cotizacion::where('id', $cotizacion_id)
            ->update(['estado' => 'aprobada']);
    }
    
    // 6. Actualizar estado de nota de venta origen
    if ($nota_venta_id) {
        Venta::where('id_venta', $nota_venta_id)
            ->update(['estado' => '3']); // Vendida
    }
});
Los productos libres (descripción sin catálogo) NO afectan stock aunque afecta_stock=true. Esto previene errores en inventario.
6

Generar y Enviar XML a SUNAT

Después de guardar, aparece modal PrintOptionsModal con opciones:

Opción 1: Envío Inmediato a SUNAT

Para Facturas y Boletas:
// Frontend llama:
POST /api/ventas/{id}/enviar-sunat
Backend proceso:
// SunatService.php
public function enviarVenta(Venta $venta)
{
    // 1. Generar XML con Greenter
    $invoice = new Invoice();
    $invoice->setTipoDoc($venta->tipoDocumento->cod_sunat);
    $invoice->setSerie($venta->serie);
    $invoice->setCorrelativo($venta->numero);
    
    // Cliente
    $client = new Client();
    $client->setTipoDoc($cliente->tipo_doc);
    $client->setNumDoc($cliente->documento);
    $client->setRznSocial($cliente->datos);
    
    // Productos
    foreach ($venta->productosVentas as $detalle) {
        $item = new SaleDetail();
        $item->setCodProducto($detalle->codigo_producto);
        $item->setDescripcion($detalle->descripcion);
        $item->setCantidad($detalle->cantidad);
        $item->setMtoValorUnitario($detalle->precio_unitario);
        $item->setTipAfeIgv($detalle->tipo_afectacion_igv);
        $invoice->addDetail($item);
    }
    
    // Forma de pago
    if ($venta->id_tipo_pago == 1) {
        $invoice->setFormaPago(new FormaPagoContado());
    } else {
        $credito = new FormaPagoCredito();
        $credito->setMonto($venta->total);
        
        foreach ($venta->cuotas as $cuota) {
            $cuotaGreenter = new Cuota();
            $cuotaGreenter->setMonto($cuota->monto);
            $cuotaGreenter->setFechaPago($cuota->fecha_vencimiento);
            $credito->addCuota($cuotaGreenter);
        }
        $invoice->setFormaPago($credito);
    }
    
    // 2. Firmar y enviar
    $see = $this->getSee($empresa);
    $result = $see->send($invoice);
    
    // 3. Procesar CDR
    if ($result->isSuccess()) {
        $cdr = $result->getCdrResponse();
        
        // Guardar CDR ZIP
        $cdrPath = "sunat/cdr/{$ruc}/R-{$nombreXml}.zip";
        Storage::put($cdrPath, $result->getCdrZip());
        
        // Actualizar venta
        $venta->update([
            'nombre_xml' => $nombreXml,
            'xml_url' => "sunat/xml/{$ruc}/{$nombreXml}.xml",
            'cdr_url' => $cdrPath,
            'estado_sunat' => '1', // Aceptado
            'codigo_sunat' => $cdr->getCode(),
            'mensaje_sunat' => $cdr->getDescription(),
        ]);
    }
}
El CDR (Constancia de Recepción) es la respuesta de SUNAT que valida el comprobante. Código 0 = Aceptado, 4XXX = Observado pero válido, 2XXX = Rechazado.

Opción 2: Solo Generar PDF (sin SUNAT)

Para Notas de Venta o ventas que no requieren envío inmediato:
// Ruta: /reporteNV/a4.php?id={venta_id}
// Genera PDF con mPDF usando plantilla A4
7

Descuento de Stock Real (Opcional)

Para ventas de Almacén 1, se puede descontar posteriormente del Almacén 2 (Kardex Real):Endpoint: POST /api/ventas/{id}/descontar-stock
// VentasController.php línea 537-599
public function descontarStock($id)
{
    $venta = Venta::with('productosVentas')
        ->where('stock_real_descontado', false)
        ->findOrFail($id);
    
    foreach ($venta->productosVentas as $detalle) {
        // Buscar producto equivalente en almacén 2
        $productoAlmacen2 = Producto::where('almacen', '2')
            ->where('codigo', $detalle->producto->codigo)
            ->first();
        
        if ($productoAlmacen2) {
            $productoAlmacen2->decrement('cantidad', $detalle->cantidad);
            
            MovimientoStock::create([
                'tipo_movimiento' => 'salida',
                'tipo_documento' => 'descuento_almacen',
                'motivo' => 'Descuento de almacén real por venta',
                'id_almacen' => 2,
            ]);
        }
    }
    
    $venta->update(['stock_real_descontado' => true]);
}
UI: Modal DescontarStockModal.jsx muestra preview de productos en Almacén 2.
8

Gestionar Cobros

Para ventas al crédito

Módulo: /finanzas/cuentas-por-cobrarRegistrar pago de cuota:
// Actualiza tabla dias_ventas
UPDATE dias_ventas 
SET estado = 'C' // Cancelado
WHERE id = cuota_id;
Estados de cuotas:
  • P = Pendiente
  • C = Cancelado
  • V = Vencido (calculado dinámicamente si fecha < hoy)

Reporte de cobranzas

Descarga Excel: /api/cuentas-cobrar/export/excel?fecha_inicio=...&fecha_fin=...Incluye:
  • Cliente, RUC/DNI
  • Documento (serie-número)
  • Fecha emisión y vencimiento
  • Monto cuota y estado
  • Días de mora

Movimientos de Stock

Tabla: movimientos_stock

CREATE TABLE movimientos_stock (
    id_producto INT,
    tipo_movimiento ENUM('entrada', 'salida'),
    cantidad DECIMAL,
    stock_anterior DECIMAL,
    stock_nuevo DECIMAL,
    tipo_documento VARCHAR(50), -- 'venta', 'compra', 'anulacion_venta', etc.
    id_documento INT,
    documento_referencia VARCHAR(50), -- 'F001-000123'
    motivo TEXT,
    id_almacen INT,
    fecha_movimiento DATETIME
);

Tipos de movimientos para ventas:

Tipotipo_movimientotipo_documentoDescripción
VentasalidaventaDescuento automático al crear venta
Anulaciónentradaanulacion_ventaRetorno al anular venta
Descuento Realsalidadescuento_almacenDescuento manual de Almacén 2

Impresión y Documentos

Formatos disponibles

PDF A4: /reporteNV/a4.php?id={venta_id}
  • Formato fiscal completo
  • QR de validación SUNAT
  • Detalles de cliente, productos, totales
  • Pie de página con forma de pago
PDF Ticket: /reporteNV/ticket.php?id={venta_id}
  • Formato térmico 80mm
  • Para impresoras POS
XML: /api/ventas/xml/{nombre_xml}.xml
  • Descarga XML firmado
  • Formato UBL 2.1 estándar SUNAT
CDR: /api/ventas/cdr/{venta_id}
  • Descarga ZIP con respuesta SUNAT
  • Contiene R-{ruc}-{tipo}-{serie}-{numero}.xml

Impresión automática

// PrintOptionsModal.jsx
const handlePrint = (formato) => {
    const token = localStorage.getItem('auth_token');
    const url = baseUrl(`/reporteNV/${formato}.php?id=${ventaId}&token=${token}`);
    window.open(url, '_blank');
};
Los PDFs se generan con mPDF en el backend. Las rutas usan el middleware TokenFromQuery para autenticar con ?token= en la URL.

Errores Comunes y Soluciones

Causa: Intentando emitir factura con DNI o sin documento.Solución:
  • Cambiar a Boleta (B001)
  • O ingresar RUC válido del cliente
Código: VentasController.php:136-141
Causa: Intentando emitir boleta con RUC.Solución: Las boletas son para consumidores finales. Use DNI o cambie a Factura.Código: VentasController.php:143-148
Causa: Producto sin stock suficiente en el almacén seleccionado.Solución:
  • Verificar stock antes de vender
  • Usar Nota de Venta (NV01) si no afecta stock
  • Ajustar inventario en módulo Almacén
Prevención: El frontend filtra productos soloConStock excepto para Notas de Venta.
Causa: Problemas de conexión o certificado inválido.Solución:
  1. Verificar certificado PEM en storage/app/sunat/certificados/
  2. Revisar credenciales SOL en Configuración → Empresa
  3. Verificar modo (beta/producción) correcto
  4. Reenviar desde lista de ventas: botón “Enviar SUNAT”
Logs: storage/logs/laravel.log con tag SUNAT -
Causa: Venta configurada como Contado en lugar de Crédito.Solución:
  • Al crear venta, seleccionar “Crédito” en Tipo de Pago
  • Definir cuotas en modal Payment Schedule
  • Verificar que id_tipo_pago = 2 en BD

Flujos Alternativos

De Nota de Venta a Factura/Boleta

  1. Crear Nota de Venta (NV01) → No afecta stock real
  2. Cliente decide comprar
  3. Desde lista de Notas de Venta: “Convertir a Factura/Boleta”
  4. URL: /ventas/crear?tipo=factura&nota_venta_id=456
  5. Sistema copia productos y marca nota como estado=3 (Vendida)
  6. Nueva venta descuenta stock de Almacén 1

Venta con productos de múltiples almacenes

No soportado directamente. Workaround:
  1. Crear venta con productos de Almacén 1
  2. Después de guardar, usar “Descontar Stock Real” para Almacén 2
  3. O crear ajustes manuales en módulo Inventario

Referencias Técnicas

Controlador principal: app/Http/Controllers/VentasController.php Servicios:
  • app/Services/SunatService.php - Integración SUNAT
  • app/Services/ProductoService.php - Gestión de stock
Modelos:
  • app/Models/Venta.php
  • app/Models/ProductoVenta.php (detalle)
  • app/Models/VentaPago.php
  • app/Models/MovimientoStock.php
Frontend:
  • resources/js/components/Facturacion/Ventas/VentaForm.jsx
  • resources/js/components/Facturacion/Ventas/hooks/useVentaForm.js
  • resources/js/components/shared/ProductoFormSection.jsx
  • resources/js/components/shared/MetodoPago.jsx
Rutas API:
POST   /api/ventas                    // Crear venta
GET    /api/ventas/{id}               // Ver detalle
POST   /api/ventas/{id}/enviar-sunat  // Enviar a SUNAT
POST   /api/ventas/{id}/anular         // Anular venta
POST   /api/ventas/{id}/descontar-stock // Descontar de Almacén 2
GET    /api/ventas/proximo-numero      // Obtener siguiente número

Build docs developers (and LLMs) love