[12, 13], 'c_moneda' => [4, 5], 'c_codigopostal' => [5, 6], 'c_regimenfiscal' => [4, 5], 'c_usocfdi' => [4, 5], 'c_claveprodserv' => [5, 6], 'c_claveunidad' => [4, 5], 'c_aduana' => [2, 3], 'c_estado' => [3], 'c_localidad' => [3], 'c_municipio' => [3], ]; /** * Convierte hojas de un XLSX o XLS a un CSV optimizado. */ public static function convertToCsv(string $filePath, $sheetNames, string $csvName, int $skipRows = 7) { $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $reader = ($extension === 'xls') ? new XlsReader() : new XlsxReader(); try { $reader->setReadDataOnly(true); $reader->setLoadSheetsOnly($sheetNames); $spreadsheet = $reader->load($filePath); } catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) { die("❌ Error al cargar hojas: " . implode(", ", (array) $sheetNames) . "\n"); } $csvPath = database_path("seeders/sat_cache/{$csvName}.csv"); $handle = fopen($csvPath, 'w'); if (!$handle) { return ["error" => "No se pudo crear el archivo CSV."]; } foreach ((array) $sheetNames as $sheetName) { if (!$spreadsheet->sheetNameExists($sheetName)) { echo "⚠️ Hoja no encontrada: {$sheetName}, saltando...\n"; continue; } $sheet = $spreadsheet->getSheetByName($sheetName); if (!$sheet) continue; $dateColumns = self::$dateColumnsMap[$csvName] ?? []; // Índices de columnas de fechas foreach ($sheet->getRowIterator($skipRows + 1) as $row) { $cells = []; $colIndex = 0; foreach ($row->getCellIterator() as $cell) { $value = trim((string) $cell->getValue()); // 🛑 Omitir filas con "Continúa en..." if (str_starts_with($value, 'Continúa en ')) { echo "⚠️ Fila omitida: {$value}\n"; continue 2; // Salta toda la fila } // 🔥 Convertir "?" y valores vacíos a NULL if ($value === '?' || $value === '') { $value = ''; } // 📅 Si es fecha de Excel, convertir a YYYY-MM-DD if (in_array($colIndex, $dateColumns) && self::isExcelDate($value)) { $value = self::convertExcelDate($value); } // 🔹 Escapar valores que contienen punto y coma, saltos de línea o comillas $value = self::escapeCsvValue($value); $cells[] = $value; $colIndex++; } if (self::isEmptyRow($cells)) { echo "⚠️ Fila vacía detectada y omitida\n"; continue; } fputcsv($handle, $cells, ';'); // 💡 Asegura que los valores se escapen correctamente } } fclose($handle); $spreadsheet->disconnectWorksheets(); unset($spreadsheet); return $csvPath; } /** * **Detecta si el valor es un número de fecha de Excel** */ private static function isExcelDate($value): bool { return is_numeric($value) && $value > 10000; } /** * **Convierte un número de fecha de Excel a formato YYYY-MM-DD** */ private static function convertExcelDate($value): string { try { return Carbon::instance(ExcelDate::excelToDateTimeObject($value))->format('Y-m-d'); } catch (\Exception $e) { return $value; } } /** * **Escapa valores con punto y coma, comillas o saltos de línea en CSV** */ private static function escapeCsvValue($value): string { // Si el valor contiene punto y coma, salto de línea o comillas dobles, se escapa if (strpbrk($value, ";\"\n")) { $value = '"' . str_replace('"', '""', $value) . '"'; // Duplicar comillas internas } return $value; } /** * **Verifica si una fila está vacía o contiene valores inválidos.** */ private static function isEmptyRow(array $cells): bool { $empty = empty(array_filter($cells, fn($cell) => $cell !== '')); if ($empty) { echo "⚠️ Fila detectada como vacía y omitida\n"; } return $empty; } /** * **Cuenta las filas de un archivo CSV.** */ public static function countCsvRows(string $csvPath): int { if (!file_exists($csvPath)) { return 0; } $file = fopen($csvPath, 'r'); $count = 0; while (fgetcsv($file, 0, ';') !== false) { $count++; } fclose($file); return $count; } }