laravel-sat-catalogs/Services/CsvGeneratorService.php

172 lines
5.3 KiB
PHP
Raw Normal View History

2025-03-10 18:25:41 -06:00
<?php
namespace Koneko\SatCatalogs\Services;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use PhpOffice\PhpSpreadsheet\Reader\Xls as XlsReader;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
use Carbon\Carbon;
class CsvGeneratorService
{
private static array $dateColumnsMap = [
'c_formapago' => [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;
}
}