172 lines
5.3 KiB
PHP
172 lines
5.3 KiB
PHP
|
<?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;
|
||
|
}
|
||
|
}
|