Preparando proyecto para migración a repositorio MVE

This commit is contained in:
2025-10-23 08:48:00 -06:00
parent b1b9e4a77c
commit d0af739ce2
75 changed files with 15875 additions and 4022 deletions

View File

@@ -0,0 +1,636 @@
<?php
session_start();
require_once __DIR__ . '/../config/database.php';
// Headers para AJAX
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
// Log para debugging
error_log("=== INICIO IMPORTACIÓN ===");
error_log("Usuario ID en sesión: " . (isset($_SESSION['usuario_id']) ? $_SESSION['usuario_id'] : 'NO_USUARIO'));
error_log("Tipo usuario: " . (isset($_SESSION['tipo_usuario']) ? $_SESSION['tipo_usuario'] : 'NO_TIPO'));
error_log("Método: " . $_SERVER['REQUEST_METHOD']);
error_log("Archivos recibidos: " . print_r($_FILES, true));
// Verificar que el usuario esté logueado
if (!isset($_SESSION['usuario_id'])) {
error_log("ERROR: Usuario no autenticado - no hay usuario_id en sesión");
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Usuario no autenticado']);
exit;
}
// Verificar que sea una petición POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Método no permitido']);
exit;
}
// Verificar que se haya subido un archivo
if (!isset($_FILES['archivo']) || $_FILES['archivo']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'No se ha recibido ningún archivo válido']);
exit;
}
class ImportadorPedimentos {
private $conn;
private $estadisticas = [
'total_lineas' => 0,
'pedimentos_procesados' => 0,
'facturas_procesadas' => 0,
'partidas_procesadas' => 0,
'errores' => 0,
'duplicados' => 0
];
private $errores_detalle = [];
private $pedimentos_creados = [];
public function __construct($connection) {
$this->conn = $connection;
}
public function procesarArchivo($archivo_path, $validar_duplicados = true) {
$inicio_tiempo = time();
$nombre_archivo = basename($archivo_path);
try {
$contenido = file_get_contents($archivo_path);
if ($contenido === false) {
throw new Exception("No se pudo leer el archivo");
}
// Convertir encoding si es necesario (muchos archivos julianos usan Latin1)
if (!mb_check_encoding($contenido, 'UTF-8')) {
$contenido = mb_convert_encoding($contenido, 'UTF-8', 'ISO-8859-1');
}
$lineas = explode("\n", $contenido);
$this->estadisticas['total_lineas'] = count($lineas);
$pedimento_actual = null;
$facturas_pedimento = [];
$partidas_pedimento = [];
foreach ($lineas as $numero_linea => $linea) {
$linea = trim($linea);
if (empty($linea)) continue;
try {
$codigo = substr($linea, 0, 3);
switch ($codigo) {
case '500':
// Línea de header - ignorar por ahora
break;
case '501':
// Si hay un pedimento anterior, procesarlo
if ($pedimento_actual) {
$this->procesarPedimento($pedimento_actual, $facturas_pedimento, $partidas_pedimento, $validar_duplicados);
$facturas_pedimento = [];
$partidas_pedimento = [];
}
$pedimento_actual = $this->parsearLinea501Real($linea);
break;
case '505':
if ($pedimento_actual) {
try {
$factura = $this->parsearLinea505Real($linea);
$facturas_pedimento[] = $factura;
error_log("Factura 505 procesada exitosamente: " . $factura['numero_factura']);
} catch (Exception $e) {
error_log("ERROR procesando línea 505: " . $e->getMessage());
error_log("Línea problemática: " . $linea);
$this->estadisticas['errores']++;
// Continuar con la siguiente línea
}
}
break;
case '551':
if ($pedimento_actual) {
try {
$partida = $this->parsearLinea551Real($linea);
$partidas_pedimento[] = $partida;
error_log("Partida 551 procesada exitosamente: secuencia " . $partida['secuencia']);
} catch (Exception $e) {
error_log("ERROR procesando línea 551: " . $e->getMessage());
error_log("Línea problemática: " . $linea);
$this->estadisticas['errores']++;
// Continuar con la siguiente línea en lugar de fallar completamente
}
}
break;
case '506':
case '509':
case '510':
case '511':
case '553':
case '554':
case '556':
case '557':
case '558':
case '800':
case '801':
// Otros códigos del formato real - ignorar por ahora
break;
}
} catch (Exception $e) {
$this->errores_detalle[] = "Línea " . ($numero_linea + 1) . ": " . $e->getMessage();
$this->estadisticas['errores']++;
}
}
// Procesar el último pedimento
if ($pedimento_actual) {
$this->procesarPedimento($pedimento_actual, $facturas_pedimento, $partidas_pedimento, $validar_duplicados);
}
// Registrar log de importación
$tiempo_procesamiento = time() - $inicio_tiempo;
$this->registrarLog($nombre_archivo, $tiempo_procesamiento,
count($this->errores_detalle) > 0 ? 'con_errores' : 'exitoso');
return [
'success' => true,
'estadisticas' => $this->estadisticas,
'errores' => $this->errores_detalle,
'pedimentos_creados' => $this->pedimentos_creados
];
} catch (Exception $e) {
// Registrar log de error
$tiempo_procesamiento = time() - $inicio_tiempo;
$this->registrarLog($nombre_archivo, $tiempo_procesamiento, 'fallido', $e->getMessage());
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
private function registrarLog($archivo_nombre, $tiempo_procesamiento, $estado, $error_mensaje = null) {
try {
$sql = "INSERT INTO importacion_logs (
usuario_id, archivo_nombre, total_lineas, pedimentos_procesados,
facturas_procesadas, partidas_procesadas, errores, duplicados,
tiempo_procesamiento, estado, detalles_errores
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$detalles_errores = null;
if (!empty($this->errores_detalle)) {
$detalles_errores = implode("\n", $this->errores_detalle);
} else if ($error_mensaje) {
$detalles_errores = $error_mensaje;
}
$params = [
isset($_SESSION['usuario_id']) ? $_SESSION['usuario_id'] : null,
$archivo_nombre,
$this->estadisticas['total_lineas'],
$this->estadisticas['pedimentos_procesados'],
$this->estadisticas['facturas_procesadas'],
$this->estadisticas['partidas_procesadas'],
$this->estadisticas['errores'],
$this->estadisticas['duplicados'],
$tiempo_procesamiento,
$estado,
$detalles_errores
];
$stmt = sqlsrv_query($this->conn, $sql, $params);
if ($stmt) {
sqlsrv_free_stmt($stmt);
}
} catch (Exception $e) {
// No hacer nada si falla el log, no queremos interrumpir el proceso principal
}
}
private function parsearLinea501($linea) {
// Formato: 501|numero_pedimento|patente|aduana|anio|clave_documento|rfc_importador|...
$campos = explode('|', $linea);
if (count($campos) < 7) {
throw new Exception("Formato inválido en línea 501");
}
return [
'numero_pedimento' => trim($campos[1]),
'patente' => trim($campos[2]),
'aduana' => trim($campos[3]),
'anio' => trim($campos[4]),
'clave_documento' => trim($campos[5]),
'rfc_importador' => trim($campos[6]),
'fecha_creacion' => date('Y-m-d H:i:s'),
'usuario_id' => $_SESSION['usuario_id']
];
}
private function parsearLinea501Real($linea) {
// Formato real: 501|patente|numero_pedimento|aduana|tipo|clave_documento|aduana2||rfc_importador|nombre_importador|...
$campos = explode('|', $linea);
if (count($campos) < 10) {
throw new Exception("Formato inválido en línea 501 real - campos insuficientes: " . count($campos));
}
error_log("Parseando 501 real: " . $linea);
error_log("Campos: " . print_r(array_slice($campos, 0, 10), true));
return [
'numero_pedimento' => trim($campos[2]), // Campo 2: numero de pedimento
'patente' => trim($campos[1]), // Campo 1: patente
'aduana' => trim($campos[3]), // Campo 3: aduana
'anio' => date('Y'), // Usar año actual
'clave_documento' => trim($campos[5]), // Campo 5: clave documento
'rfc_importador' => trim($campos[8]), // Campo 8: RFC importador
'fecha_creacion' => date('Y-m-d H:i:s'),
'usuario_id' => $_SESSION['usuario_id']
];
}
private function parsearLinea505($linea) {
// Formato: 505|numero_factura|fecha_factura|valor_dolares|valor_factura|...
$campos = explode('|', $linea);
if (count($campos) < 5) {
throw new Exception("Formato inválido en línea 505");
}
return [
'numero_factura' => trim($campos[1]),
'fecha_factura' => $this->convertirFecha(trim($campos[2])),
'valor_dolares' => floatval(trim($campos[3])),
'valor_factura' => floatval(trim($campos[4]))
];
}
private function parsearLinea505Real($linea) {
// Formato oficial 505: 505|numero_pedimento|fecha_cfdi|numero_cfdi_cove|termino_facturacion|moneda|valor_dolares|valor_total|pais|entidad_federativa|rfc_proveedor|nombre_proveedor|calle|num_int|num_ext|cp|municipio|...
$campos = explode('|', $linea);
if (count($campos) < 12) {
throw new Exception("Formato inválido en línea 505 - campos insuficientes: " . count($campos) . " (mínimo: 12)");
}
error_log("DEBUG 505 - Total campos: " . count($campos));
error_log("DEBUG 505 - Primeros 12 campos: " . implode(' | ', array_slice($campos, 0, 12)));
// Extraer datos según documentación oficial SAT
$numero_pedimento = trim($campos[1]); // Campo 1: Número de Pedimento
$fecha_cfdi = trim($campos[2]); // Campo 2: Fecha de CFDI
$numero_cfdi_cove = trim($campos[3]); // Campo 3: Número de CFDI (COVE) - 40 caracteres max
$termino_facturacion = trim($campos[4]); // Campo 4: Término de Facturación - 3 caracteres
$moneda = trim($campos[5]); // Campo 5: Moneda - 3 caracteres
$valor_dolares = floatval(trim($campos[6])); // Campo 6: Valor Total en Dólares USD
$valor_total = floatval(trim($campos[7])); // Campo 7: Valor Total en moneda del CFDI
$pais = trim($campos[8]); // Campo 8: País del CFDI - 3 caracteres
$entidad_federativa = trim($campos[9]); // Campo 9: Entidad Federativa - 3 caracteres
$rfc_proveedor = trim($campos[10]); // Campo 10: RFC Proveedor - 30 caracteres max
$nombre_proveedor = trim($campos[11]); // Campo 11: Nombre Proveedor - 120 caracteres max
error_log("DEBUG 505 - Datos oficiales extraídos:");
error_log(" - Número CFDI/COVE: '$numero_cfdi_cove'");
error_log(" - Fecha CFDI: '$fecha_cfdi'");
error_log(" - Término facturación: '$termino_facturacion'");
error_log(" - Moneda: '$moneda'");
error_log(" - Valor USD: $valor_dolares");
error_log(" - Valor total: $valor_total");
error_log(" - RFC Proveedor: '$rfc_proveedor'");
error_log(" - Nombre Proveedor: '$nombre_proveedor'");
return [
'numero_factura' => $numero_cfdi_cove, // Campo 3: Número de CFDI/COVE
'fecha_factura' => $this->convertirFechaReal($fecha_cfdi), // Campo 2: Fecha CFDI
'valor_dolares' => $valor_dolares, // Campo 6: Valor en USD
'valor_factura' => $valor_total, // Campo 7: Valor total en moneda CFDI
'cove' => $numero_cfdi_cove, // Campo 3: COVE (mismo que número factura)
'moneda' => $moneda, // Campo 5: Moneda
'proveedor' => $nombre_proveedor // Campo 11: Nombre del proveedor
];
}
private function parsearLinea551($linea) {
// Formato: 551|secuencia|fraccion_arancelaria|descripcion|cantidad|unidad|valor_unitario|...
$campos = explode('|', $linea);
if (count($campos) < 7) {
throw new Exception("Formato inválido en línea 551");
}
return [
'secuencia' => intval(trim($campos[1])),
'fraccion_arancelaria' => trim($campos[2]),
'descripcion' => trim($campos[3]),
'cantidad' => floatval(trim($campos[4])),
'unidad' => trim($campos[5]),
'valor_unitario' => floatval(trim($campos[6]))
];
}
private function parsearLinea551Real($linea) {
// Formato oficial 551: 551|numero_pedimento|fraccion_arancelaria|numero_partida|subdivision|descripcion|precio_unitario|valor_aduana|valor_comercial|valor_dolares|cantidad_umc|unidad_comercial|...
$campos = explode('|', $linea);
if (count($campos) < 12) {
throw new Exception("Formato inválido en línea 551 - campos insuficientes: " . count($campos) . " (mínimo: 12)");
}
error_log("DEBUG 551 - Total campos: " . count($campos));
error_log("DEBUG 551 - Primeros 12 campos: " . implode(' | ', array_slice($campos, 0, 12)));
// Extraer datos según documentación oficial
$numero_pedimento = isset($campos[1]) ? trim($campos[1]) : '';
$fraccion_arancelaria = isset($campos[2]) ? trim($campos[2]) : '';
$numero_partida = isset($campos[3]) ? intval(trim($campos[3])) : 0;
$subdivision = isset($campos[4]) ? trim($campos[4]) : '';
$descripcion = isset($campos[5]) ? trim($campos[5]) : '';
$precio_unitario = isset($campos[6]) ? floatval(trim($campos[6])) : 0;
$valor_aduana = isset($campos[7]) ? floatval(trim($campos[7])) : 0;
$valor_comercial = isset($campos[8]) ? floatval(trim($campos[8])) : 0;
$valor_dolares = isset($campos[9]) ? floatval(trim($campos[9])) : 0;
$cantidad_umc = isset($campos[10]) ? floatval(trim($campos[10])) : 0;
$unidad_comercial = isset($campos[11]) ? trim($campos[11]) : '';
// Aplicar valores por defecto según documentación oficial
if (empty($fraccion_arancelaria)) {
error_log("WARNING 551: Fracción arancelaria vacía, usando valor por defecto");
$fraccion_arancelaria = 'PENDIENTE';
}
if ($numero_partida <= 0) {
error_log("WARNING 551: Número de partida inválido, generando secuencial");
static $contador_partida = 1;
$numero_partida = $contador_partida++;
}
if (empty($descripcion)) {
error_log("WARNING 551: Descripción vacía, usando valor por defecto");
$descripcion = 'DESCRIPCIÓN PENDIENTE';
}
if ($cantidad_umc <= 0) {
error_log("WARNING 551: Cantidad UMC inválida ($cantidad_umc), usando 1.0000");
$cantidad_umc = 1.0000;
}
if (empty($unidad_comercial)) {
error_log("WARNING 551: Unidad comercial vacía, usando 'PZ'");
$unidad_comercial = 'PZ'; // Pieza como unidad por defecto
}
// Calcular precio unitario si no existe pero hay valores
if ($precio_unitario == 0) {
if ($valor_dolares > 0 && $cantidad_umc > 0) {
$precio_unitario = $valor_dolares / $cantidad_umc;
error_log("DEBUG 551: Precio unitario calculado: $precio_unitario");
} elseif ($valor_comercial > 0 && $cantidad_umc > 0) {
$precio_unitario = $valor_comercial / $cantidad_umc;
error_log("DEBUG 551: Precio unitario calculado desde valor comercial: $precio_unitario");
}
}
error_log("DEBUG 551 - Datos procesados:");
error_log(" - Partida: $numero_partida");
error_log(" - Fracción: '$fraccion_arancelaria'");
error_log(" - Descripción: '" . substr($descripcion, 0, 50) . "...'");
error_log(" - Cantidad: $cantidad_umc");
error_log(" - Unidad: '$unidad_comercial'");
error_log(" - Precio unitario: $precio_unitario");
error_log(" - Valor dólares: $valor_dolares");
return [
'secuencia' => $numero_partida,
'fraccion_arancelaria' => $fraccion_arancelaria,
'descripcion' => $descripcion,
'cantidad' => $cantidad_umc,
'unidad' => $unidad_comercial,
'valor_unitario' => $precio_unitario,
'peso_neto' => null,
'peso_bruto' => null
];
}
private function procesarPedimento($pedimento, $facturas, $partidas, $validar_duplicados) {
try {
error_log("Procesando pedimento: " . $pedimento['numero_pedimento']);
error_log("Validar duplicados: " . ($validar_duplicados ? 'SÍ' : 'NO'));
// Validar duplicados si está habilitado
if ($validar_duplicados && $this->existePedimento($pedimento['numero_pedimento'])) {
error_log("Pedimento {$pedimento['numero_pedimento']} marcado como duplicado, omitiendo...");
$this->estadisticas['duplicados']++;
return;
}
error_log("Insertando pedimento nuevo: " . $pedimento['numero_pedimento']);
// Iniciar transacción
sqlsrv_begin_transaction($this->conn);
// Insertar pedimento
$sql = "INSERT INTO pedimentos (numero_pedimento, patente, aduana, anio, clave_documento,
rfc_importador, fecha_creacion, usuario_id, estado)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'activo'); SELECT SCOPE_IDENTITY() AS id;";
$params = [
$pedimento['numero_pedimento'],
$pedimento['patente'],
$pedimento['aduana'],
$pedimento['anio'],
$pedimento['clave_documento'],
$pedimento['rfc_importador'],
$pedimento['fecha_creacion'],
$pedimento['usuario_id']
];
$stmt = sqlsrv_query($this->conn, $sql, $params);
if ($stmt === false) {
throw new Exception("Error al insertar pedimento: " . print_r(sqlsrv_errors(), true));
}
// Obtener el ID del pedimento insertado
sqlsrv_next_result($stmt);
sqlsrv_fetch($stmt);
$pedimento_id = sqlsrv_get_field($stmt, 0);
sqlsrv_free_stmt($stmt);
// Insertar facturas
foreach ($facturas as $factura) {
$sql = "INSERT INTO pedimento_facturas (pedimento_id, numero_factura, fecha_factura,
valor_dolares, valor_factura, cove)
VALUES (?, ?, ?, ?, ?, ?)";
$params = [
$pedimento_id,
$factura['numero_factura'],
$factura['fecha_factura'],
$factura['valor_dolares'],
$factura['valor_factura'],
isset($factura['cove']) ? $factura['cove'] : null
];
$stmt = sqlsrv_query($this->conn, $sql, $params);
if ($stmt === false) {
throw new Exception("Error al insertar factura: " . print_r(sqlsrv_errors(), true));
}
sqlsrv_free_stmt($stmt);
$this->estadisticas['facturas_procesadas']++;
}
// Insertar partidas
foreach ($partidas as $partida) {
error_log("INSERTANDO PARTIDA: " . print_r($partida, true));
$sql = "INSERT INTO pedimento_partidas (pedimento_id, secuencia, fraccion_arancelaria,
descripcion, cantidad, unidad, valor_unitario,
peso_neto, peso_bruto)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$params = [
$pedimento_id,
$partida['secuencia'],
$partida['fraccion_arancelaria'],
$partida['descripcion'],
$partida['cantidad'],
$partida['unidad'],
$partida['valor_unitario'],
isset($partida['peso_neto']) ? $partida['peso_neto'] : null,
isset($partida['peso_bruto']) ? $partida['peso_bruto'] : null
];
error_log("PARÁMETROS SQL: " . print_r($params, true));
$stmt = sqlsrv_query($this->conn, $sql, $params);
if ($stmt === false) {
$error = print_r(sqlsrv_errors(), true);
error_log("ERROR AL INSERTAR PARTIDA: " . $error);
throw new Exception("Error al insertar partida: " . $error);
}
sqlsrv_free_stmt($stmt);
$this->estadisticas['partidas_procesadas']++;
error_log("Partida insertada exitosamente - Total procesadas: " . $this->estadisticas['partidas_procesadas']);
}
// Confirmar transacción
sqlsrv_commit($this->conn);
$this->estadisticas['pedimentos_procesados']++;
$this->pedimentos_creados[] = $pedimento['numero_pedimento'];
} catch (Exception $e) {
sqlsrv_rollback($this->conn);
$this->errores_detalle[] = "Error procesando pedimento {$pedimento['numero_pedimento']}: " . $e->getMessage();
$this->estadisticas['errores']++;
}
}
private function existePedimento($numero_pedimento) {
$sql = "SELECT id, fecha_creacion FROM pedimentos WHERE numero_pedimento = ?";
$stmt = sqlsrv_query($this->conn, $sql, [$numero_pedimento]);
if ($stmt === false) {
error_log("Error al verificar duplicado: " . print_r(sqlsrv_errors(), true));
return false;
}
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmt);
if ($row) {
error_log("Pedimento duplicado encontrado: {$numero_pedimento} (ID: {$row['id']})");
return true;
}
return false;
}
private function convertirFecha($fecha_str) {
// Convertir formato DDMMYYYY a YYYY-MM-DD
if (strlen($fecha_str) === 8) {
$dia = substr($fecha_str, 0, 2);
$mes = substr($fecha_str, 2, 2);
$anio = substr($fecha_str, 4, 4);
return "$anio-$mes-$dia";
}
return date('Y-m-d'); // Fecha por defecto
}
private function convertirFechaReal($fecha_str) {
// Convertir fecha del archivo real - puede venir en formato YYYYMMDD o DDMMYYYY
if (empty($fecha_str) || strlen($fecha_str) !== 8) {
return date('Y-m-d'); // Fecha por defecto
}
// Intentar formato YYYYMMDD primero
if (substr($fecha_str, 0, 2) === '20') {
$anio = substr($fecha_str, 0, 4);
$mes = substr($fecha_str, 4, 2);
$dia = substr($fecha_str, 6, 2);
} else {
// Formato DDMMYYYY
$dia = substr($fecha_str, 0, 2);
$mes = substr($fecha_str, 2, 2);
$anio = substr($fecha_str, 4, 4);
}
// Validar fecha
if (checkdate($mes, $dia, $anio)) {
return "$anio-$mes-$dia";
}
return date('Y-m-d'); // Fecha por defecto si no es válida
}
}
// Procesar la importación
try {
$archivo = $_FILES['archivo'];
$validar_duplicados = isset($_POST['validar_duplicados']) && $_POST['validar_duplicados'] === 'on';
// Crear directorio temporal si no existe
$upload_dir = __DIR__ . '/../storage/temp/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
// Mover archivo a directorio temporal
$archivo_temporal = $upload_dir . 'import_' . time() . '_' . $archivo['name'];
if (!move_uploaded_file($archivo['tmp_name'], $archivo_temporal)) {
throw new Exception("Error al procesar el archivo subido");
}
// Obtener conexión a SQL Server
$conn = getConnection();
// Procesar archivo
$importador = new ImportadorPedimentos($conn);
$resultado = $importador->procesarArchivo($archivo_temporal, $validar_duplicados);
// Cerrar conexión
sqlsrv_close($conn);
// Limpiar archivo temporal
unlink($archivo_temporal);
// Enviar respuesta
echo json_encode($resultado);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Error del servidor: ' . $e->getMessage()
]);
}
?>