Skip to main content

Exportar a Excel y TXT

El sistema permite exportar datos en múltiples formatos para integración con sistemas contables y cumplimiento normativo SUNAT.

Formatos de Exportación

Excel (.xlsx)

PhpSpreadsheet con formato profesional

TXT (PLE)

Formato SUNAT para declaraciones

PDF

mPDF para impresión y archivo

Exportación a Excel (PhpSpreadsheet)

Instalación y Configuración

El sistema usa PhpOffice/PhpSpreadsheet para generar archivos Excel.
composer require phpoffice/phpspreadsheet
Clases principales:
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;

Estructura Básica

1

Crear spreadsheet

$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
2

Configurar título

$sheet->setCellValue('A1', 'TÍTULO DEL REPORTE');
$sheet->mergeCells('A1:J1');  // Combinar celdas
$sheet->getStyle('A1')->applyFromArray([
    'font' => [
        'bold' => true,
        'size' => 14,
        'color' => ['rgb' => 'FFFFFF']
    ],
    'fill' => [
        'fillType' => Fill::FILL_SOLID,
        'startColor' => ['rgb' => 'C7161D']  // Color corporativo
    ],
    'alignment' => [
        'horizontal' => Alignment::HORIZONTAL_CENTER,
        'vertical' => Alignment::VERTICAL_CENTER
    ]
]);
3

Agregar encabezados

$headers = ['Código', 'Producto', 'Stock', 'Precio'];
$col = 'A';
foreach ($headers as $header) {
    $sheet->setCellValue($col . '4', $header);
    $col++;
}

$sheet->getStyle('A4:D4')->applyFromArray([
    'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
    'fill' => ['fillType' => Fill::FILL_SOLID, 
               'startColor' => ['rgb' => '90BFEB']],
    'borders' => ['allBorders' => [
        'borderStyle' => Border::BORDER_THIN
    ]]
]);
4

Llenar datos

$row = 5;
foreach ($productos as $p) {
    $sheet->setCellValue('A' . $row, $p->codigo);
    $sheet->setCellValue('B' . $row, $p->nombre);
    $sheet->setCellValue('C' . $row, $p->stock);
    $sheet->setCellValue('D' . $row, $p->precio);
    
    // Filas alternadas
    if ($row % 2 === 0) {
        $sheet->getStyle("A{$row}:D{$row}")->applyFromArray([
            'fill' => ['fillType' => Fill::FILL_SOLID,
                       'startColor' => ['rgb' => 'F9FAFB']]
        ]);
    }
    
    $row++;
}
5

Formato y descarga

// Ancho automático
foreach (range('A', 'D') as $col) {
    $sheet->getColumnDimension($col)->setAutoSize(true);
}

// Formato numérico
$sheet->getStyle('D5:D' . ($row - 1))
    ->getNumberFormat()
    ->setFormatCode('#,##0.00');

// Generar archivo
$writer = new Xlsx($spreadsheet);
$filename = 'reporte-' . date('Y-m-d-His') . '.xlsx';

header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="' . $filename . '"');
header('Cache-Control: max-age=0');
$writer->save('php://output');
exit;

Formatos de Celdas

Formato Numérico

// Moneda con 2 decimales
$sheet->getStyle('E5:E100')
    ->getNumberFormat()
    ->setFormatCode('#,##0.00');

// Enteros
$sheet->getStyle('F5:F100')
    ->getNumberFormat()
    ->setFormatCode('#,##0');

// Porcentaje
$sheet->getStyle('G5:G100')
    ->getNumberFormat()
    ->setFormatCode('0.00%');

Colores Corporativos

// Paleta de colores Santo Domingo
$colores = [
    'rojo' => 'C7161D',      // Color principal
    'crema' => 'ECE6A3',     // Color acento
    'gris_claro' => 'F9FAFB',
    'gris' => 'E5E7EB',
    'azul' => '90BFEB',
    'verde' => '10B981',
    'amarillo' => 'FEF3C7'
];

Bordes

// Bordes completos
$sheet->getStyle('A4:J100')->applyFromArray([
    'borders' => [
        'allBorders' => [
            'borderStyle' => Border::BORDER_THIN,
            'color' => ['rgb' => 'CCCCCC']
        ]
    ]
]);

// Borde inferior grueso (separador)
$sheet->getStyle('A10:J10')->applyFromArray([
    'borders' => [
        'bottom' => [
            'borderStyle' => Border::BORDER_MEDIUM,
            'color' => ['rgb' => 'F59E0B']
        ]
    ]
]);

Ejemplo Real: Reporte RVTA

Exportación del Registro de Ventas e Ingresos formato SUNAT.
// VentaExportController.php:317-472
public function reporteRVTA(Request $request)
{
    [$user, $ventas, $mes, $anio] = $this->getVentasPeriodo($request);
    
    $empresa = $user->empresaActiva ?? $user->empresa;
    $ruc = $empresa->ruc ?? '';
    $periodo = $anio . str_pad($mes, 2, '0', STR_PAD_LEFT);
    
    $spreadsheet = new Spreadsheet();
    $sheet = $spreadsheet->getActiveSheet();
    $sheet->setTitle('RVTA');
    
    // Título
    $sheet->setCellValue('A1', "REGISTRO DE VENTAS E INGRESOS");
    $sheet->mergeCells('A1:R1');
    $sheet->getRowDimension(1)->setRowHeight(32);
    
    $sheet->setCellValue('A2', "PERIODO: {$meses[(int)$mes]} {$anio} | RUC: {$ruc}");
    $sheet->mergeCells('A2:R2');
    
    // 18 columnas SUNAT
    $headers = [
        'CUO', 'Fecha Emisión', 'Tipo Doc', 'Serie', 'Número',
        'Tipo Doc Cliente', 'Nro Doc Cliente', 'Razón Social',
        'Base Imponible', 'IGV', 'Exonerado', 'Inafecto', 
        'ISC', 'ICBPER', 'Otros', 'Total', 'Moneda', 'Estado'
    ];
    
    $col = 'A';
    foreach ($headers as $header) {
        $sheet->setCellValue($col . '4', $header);
        $col++;
    }
    
    // Datos
    $row = 5;
    $correlativo = 1;
    $totalBase = $totalIgv = $totalGeneral = 0;
    
    foreach ($ventas as $v) {
        $codSunat = $v->tipoDocumento->cod_sunat ?? '00';
        if ($codSunat === '00') continue;
        
        $anulada = in_array($v->estado, ['2', 'A']);
        $cuo = 'M' . str_pad($correlativo, 9, '0', STR_PAD_LEFT);
        
        $sheet->setCellValue('A' . $row, $cuo);
        $sheet->setCellValue('B' . $row, $v->fecha_emision->format('d/m/Y'));
        $sheet->setCellValue('C' . $row, $codSunat);
        // ... más columnas
        
        if (!$anulada) {
            $totalBase += $v->subtotal;
            $totalIgv += $v->igv;
            $totalGeneral += $v->total;
        }
        
        $correlativo++;
        $row++;
    }
    
    // Totales
    $sheet->setCellValue('H' . $row, 'TOTALES:');
    $sheet->setCellValue('I' . $row, $totalBase);
    $sheet->setCellValue('J' . $row, $totalIgv);
    $sheet->setCellValue('P' . $row, $totalGeneral);
    
    $filename = "RVTA-{$periodo}.xlsx";
    // ... generar y descargar
}

Exportación TXT - Formato PLE SUNAT

PLE 14.1 - Registro de Ventas

Formato de texto plano con 35 campos separados por pipe (|). Especificación: Programa de Libros Electrónicos (PLE) - SUNAT Nombre del archivo: LE{RUC}{PERIODO}1401{INDICADOR}{OPORTUNIDAD}.TXT Ejemplo: LE20612706702202401001401001111.TXT Componentes del nombre:
  • LE: Prefijo obligatorio
  • 20612706702: RUC de la empresa
  • 202401: Periodo YYYYMM
  • 00: Día (00 para mensual)
  • 140100: Código del libro (14.1 = Registro de Ventas)
  • 1: Indicador de contenido (1 = con datos, 0 = sin datos)
  • 1: Indicador de operaciones (1 = normal)
  • 1: Indicador de libro (1 = Original)
  • 1: Indicador de moneda (1 = Soles)

Estructura del Archivo TXT

Cada línea representa un comprobante con 35 campos:
  1. Periodo: YYYYMM00 (ej: 20240100)
  2. CUO: Código Único de Operación (M000000001)
  3. Correlativo Asiento: M-1
  4. Fecha Emisión: dd/mm/yyyy
  5. Fecha Vencimiento: dd/mm/yyyy (puede estar vacío)
  6. Tipo Comprobante: Código SUNAT
    • 01 = Factura
    • 03 = Boleta
    • 07 = Nota de Crédito
    • 08 = Nota de Débito
  7. Serie: F001, B001, etc.
  8. Número: 00000123 (8 dígitos con ceros a la izquierda)
  9. Número Final: Vacío (solo para consolidados)
  10. Tipo Doc Identidad: Código SUNAT (Tabla 2)
    • 0 = Otros
    • 1 = DNI
    • 4 = Carné de Extranjería
    • 6 = RUC
    • 7 = Pasaporte
  1. Número Doc Identidad: RUC/DNI del cliente
  2. Razón Social: Nombre completo o razón social
  3. Exportación: 0.00 (no aplica en Perú)
  4. Base Imponible Gravada: Subtotal sin IGV
  5. Descuento Base Imponible: 0.00
  6. IGV: 18% de la base imponible
  7. Descuento IGV: 0.00
  8. Exonerado: Monto exonerado de IGV
  9. Inafecto: Monto inafecto
  10. ISC: Impuesto Selectivo al Consumo
  1. Base IVAP: 0.00 (no aplica)
  2. IVAP: 0.00
  3. ICBPER: Impuesto a las Bolsas Plásticas
  4. Otros Tributos: 0.00
  5. Total: Monto total del comprobante
  6. Moneda: Vacío (PEN) o USD
  7. Tipo de Cambio: Vacío o tasa (ej: 3.750)
  8. Fecha Doc Modificado: Solo para notas de crédito/débito
  9. Tipo Doc Modificado: Solo para notas
  10. Serie Doc Modificado: Solo para notas
  11. Número Doc Modificado: Solo para notas
  12. Contrato/Proyecto: Vacío
  13. Error Tipo Cambio: Vacío
  14. Medio de Pago: Vacío
  15. Estado: 1 = Vigente, 2 = Anulado

Implementación

// VentaExportController.php:86-182
public function exportarTxt(Request $request)
{
    [$user, $ventas, $mes, $anio] = $this->getVentasPeriodo($request);
    
    $empresa = $user->empresaActiva ?? $user->empresa;
    $ruc = $empresa->ruc ?? '00000000000';
    $periodo = $anio . str_pad($mes, 2, '0', STR_PAD_LEFT) . '00';
    
    $lines = [];
    $correlativo = 1;
    
    foreach ($ventas as $venta) {
        $codSunat = $venta->tipoDocumento->cod_sunat ?? '00';
        if ($codSunat === '00') continue;  // Saltar documentos sin código SUNAT
        
        $cuo = 'M' . str_pad($correlativo, 9, '0', STR_PAD_LEFT);
        $fechaEmision = $venta->fecha_emision->format('d/m/Y');
        $fechaVencimiento = $venta->fecha_vencimiento 
            ? $venta->fecha_vencimiento->format('d/m/Y') 
            : '';
        
        $docCliente = $venta->cliente->documento ?? '';
        $tipoDocId = $this->getTipoDocIdentidad($docCliente);
        
        // Si está anulada, montos en 0
        $anulada = in_array($venta->estado, ['2', 'A']);
        $baseImponible = $anulada ? '0.00' : number_format($venta->subtotal ?? 0, 2, '.', '');
        $igv = $anulada ? '0.00' : number_format($venta->igv ?? 0, 2, '.', '');
        $total = $anulada ? '0.00' : number_format($venta->total ?? 0, 2, '.', '');
        
        $numero = str_pad($venta->numero, 8, '0', STR_PAD_LEFT);
        
        // Construir los 35 campos
        $campos = [
            $periodo,           // 1
            $cuo,               // 2
            'M-1',              // 3
            $fechaEmision,      // 4
            $fechaVencimiento,  // 5
            $codSunat,          // 6
            $venta->serie,      // 7
            $numero,            // 8
            '',                 // 9
            $tipoDocId,         // 10
            $docCliente,        // 11
            $venta->cliente->datos ?? '', // 12
            '0.00',             // 13
            $baseImponible,     // 14
            '0.00',             // 15
            $igv,               // 16
            '0.00',             // 17
            number_format($venta->mon_exonerado ?? 0, 2, '.', ''), // 18
            number_format($venta->mon_inafecto ?? 0, 2, '.', ''),  // 19
            '0.00',             // 20
            '0.00',             // 21
            '0.00',             // 22
            '0.00',             // 23
            '0.00',             // 24
            $total,             // 25
            $venta->tipo_moneda === 'USD' ? 'USD' : '', // 26
            $venta->tipo_cambio ? number_format($venta->tipo_cambio, 3, '.', '') : '', // 27
            '',                 // 28-32
            '',
            '',
            '',
            '',
            '',                 // 33
            '',                 // 34
            $anulada ? '2' : '1', // 35
        ];
        
        // Unir con pipe y agregar pipe final
        $lines[] = implode('|', $campos) . '|';
        $correlativo++;
    }
    
    $contenido = implode("\r\n", $lines);
    $indicadorContenido = count($lines) > 0 ? '1' : '0';
    $filename = "LE{$ruc}{$periodo}140100{$indicadorContenido}111.TXT";
    
    return response($contenido, 200, [
        'Content-Type' => 'text/plain; charset=utf-8',
        'Content-Disposition' => 'attachment; filename="' . $filename . '"'
    ]);
}

Función auxiliar: Tipo de Documento

private function getTipoDocIdentidad($documento)
{
    $len = strlen($documento ?? '');
    if ($len === 11) return '6'; // RUC
    if ($len === 8) return '1';  // DNI
    if ($len > 0) return '0';    // Otros
    return '-';
}

Exportación a PDF (mPDF)

Instalación

composer require mpdf/mpdf

Configuración Básica

use Mpdf\Mpdf;

$mpdf = new Mpdf([
    'mode' => 'utf-8',
    'format' => 'A4',           // o 'A4-L' para landscape
    'tempDir' => storage_path('app/mpdf'),
    'margin_left' => 10,
    'margin_right' => 10,
    'margin_top' => 10,
    'margin_bottom' => 15
]);

$mpdf->SetTitle('Título del PDF');
$mpdf->WriteHTML($html);

// Descargar (D) o mostrar en navegador (I)
$mpdf->Output('archivo.pdf', 'I');

Ejemplo: Reporte de Ventas

// VentaExportController.php:757-787
public function exportarPdf(Request $request)
{
    [$user, $ventas, $mes, $anio] = $this->getTodasVentasPeriodo($request);
    
    $empresa = $user->empresaActiva ?? $user->empresa;
    $meses = ['', 'Enero', 'Febrero', 'Marzo', /* ... */];
    
    // Renderizar vista Blade
    $html = view('reportes.ventas-lista', compact(
        'ventas', 'empresa', 'mes', 'anio', 'meses'
    ))->render();
    
    $mpdf = new Mpdf([
        'mode' => 'utf-8',
        'format' => 'A4-L',  // Landscape para más columnas
        'tempDir' => storage_path('app/mpdf'),
        'margin_left' => 10,
        'margin_right' => 10,
        'margin_top' => 10,
        'margin_bottom' => 15
    ]);
    
    $mpdf->SetTitle("Reporte de Ventas - {$meses[(int)$mes]} {$anio}");
    $mpdf->WriteHTML($html);
    
    return response($mpdf->Output('', 'S'), 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => "attachment; filename=\"ventas-{$anio}-{$mes}.pdf\""
    ]);
}

Vista Blade para PDF

{{-- resources/views/reportes/ventas-lista.blade.php --}}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body { font-family: Arial, sans-serif; font-size: 10px; }
        table { width: 100%; border-collapse: collapse; }
        th { background-color: #C7161D; color: white; padding: 8px; }
        td { padding: 5px; border-bottom: 1px solid #ddd; }
        .total { font-weight: bold; background-color: #FEF3C7; }
    </style>
</head>
<body>
    <h2>REPORTE DE VENTAS - {{ $meses[$mes] }} {{ $anio }}</h2>
    <p>{{ $empresa->razon_social }} | RUC: {{ $empresa->ruc }}</p>
    
    <table>
        <thead>
            <tr>
                <th>Documento</th>
                <th>Fecha</th>
                <th>Cliente</th>
                <th>Subtotal</th>
                <th>IGV</th>
                <th>Total</th>
            </tr>
        </thead>
        <tbody>
            @foreach($ventas as $v)
                <tr>
                    <td>{{ $v->tipoDocumento->abreviatura }} {{ $v->serie }}-{{ str_pad($v->numero, 8, '0', STR_PAD_LEFT) }}</td>
                    <td>{{ $v->fecha_emision->format('d/m/Y') }}</td>
                    <td>{{ $v->cliente->datos ?? 'N/A' }}</td>
                    <td>S/ {{ number_format($v->subtotal, 2) }}</td>
                    <td>S/ {{ number_format($v->igv, 2) }}</td>
                    <td>S/ {{ number_format($v->total, 2) }}</td>
                </tr>
            @endforeach
        </tbody>
        <tfoot>
            <tr class="total">
                <td colspan="5" align="right">TOTAL:</td>
                <td>S/ {{ number_format($ventas->sum('total'), 2) }}</td>
            </tr>
        </tfoot>
    </table>
</body>
</html>

Integración con Sistemas Contables

Exportar para CONCAR

Formato de texto con estructura fija.
public function exportarCONCAR(Request $request)
{
    $ventas = $this->getVentas($request);
    $lines = [];
    
    foreach ($ventas as $v) {
        // Formato fijo CONCAR
        $line = sprintf(
            "%-4s%-8s%-60s%-15s%-15s",
            $v->tipo_doc,                    // 4 caracteres
            $v->serie . $v->numero,          // 8 caracteres
            substr($v->cliente->datos, 0, 60), // 60 caracteres
            number_format($v->subtotal, 2, '.', ''), // 15 caracteres
            number_format($v->total, 2, '.', '')     // 15 caracteres
        );
        $lines[] = $line;
    }
    
    $contenido = implode("\r\n", $lines);
    return response($contenido, 200, [
        'Content-Type' => 'text/plain',
        'Content-Disposition' => 'attachment; filename="CONCAR-' . date('Ymd') . '.txt"'
    ]);
}

Exportar para SISCONT

Formato CSV con delimitadores específicos.
public function exportarSISCONT(Request $request)
{
    $ventas = $this->getVentas($request);
    
    $csv = fopen('php://temp', 'r+');
    
    // Encabezado
    fputcsv($csv, [
        'FECHA', 'TIPO', 'SERIE', 'NUMERO', 'RUC', 'CLIENTE', 
        'SUBTOTAL', 'IGV', 'TOTAL'
    ], ';');
    
    foreach ($ventas as $v) {
        fputcsv($csv, [
            $v->fecha_emision->format('d/m/Y'),
            $v->tipoDocumento->cod_sunat,
            $v->serie,
            $v->numero,
            $v->cliente->documento,
            $v->cliente->datos,
            number_format($v->subtotal, 2, '.', ''),
            number_format($v->igv, 2, '.', ''),
            number_format($v->total, 2, '.', '')
        ], ';');
    }
    
    rewind($csv);
    $output = stream_get_contents($csv);
    fclose($csv);
    
    return response($output, 200, [
        'Content-Type' => 'text/csv',
        'Content-Disposition' => 'attachment; filename="SISCONT-' . date('Ymd') . '.csv"'
    ]);
}

Solución de Problemas

Causa: Salida HTML antes de generar el Excel.Solución:
  1. Asegúrate de que no haya echo, var_dump o espacios antes de <?php
  2. Usa exit; después de $writer->save('php://output');
  3. Verifica que los headers se envíen correctamente
Causa: Formato incorrecto o campos faltantes.Solución:
  1. Verifica que cada línea tenga exactamente 35 campos
  2. Todos los campos deben terminar con |
  3. Los números deben usar punto (.) como separador decimal
  4. Las fechas deben tener formato dd/mm/yyyy
  5. El nombre del archivo debe seguir la nomenclatura exacta de SUNAT
Causa: Muchos registros o imágenes pesadas.Solución:
  1. Limita el periodo del reporte
  2. Usa paginación: máximo 1000 registros por PDF
  3. Optimiza las imágenes (si las incluyes)
  4. Para reportes grandes, recomienda usar Excel

Próximos Pasos

Reportes de Ventas

Genera reportes de ventas

Reportes Financieros

Exporta caja y bancos

Reportes de Inventario

Exporta productos y stock

API Reference

Documentación de endpoints

Build docs developers (and LLMs) love