feature/implementacion de gestor de informacion y archivos minIO

This commit is contained in:
Dulce
2026-04-22 11:10:05 -06:00
parent 69d07f2713
commit 39504e196c
23 changed files with 2272 additions and 391 deletions

View File

@@ -12,13 +12,28 @@ import json
def credenciales_to_dict(credenciales):
if not credenciales:
return {}
key_value = None
if credenciales.key:
if hasattr(credenciales.key, 'url'):
key_value = credenciales.key.url
else:
key_value = str(credenciales.key)
cer_value = None
if credenciales.cer:
if hasattr(credenciales.cer, 'url'):
cer_value = credenciales.cer.url
else:
cer_value = str(credenciales.cer)
return {
"id": str(credenciales.id),
"user": credenciales.usuario,
"password": credenciales.password,
"efirma": credenciales.efirma,
"key": credenciales.key.url if credenciales.key else None,
"cer": credenciales.cer.url if credenciales.cer else None,
"key": key_value,
"cer": cer_value,
"is_active": credenciales.is_active,
"organizacion": str(credenciales.organizacion.id) if credenciales.organizacion else None,
}

View File

@@ -61,6 +61,7 @@ except ImportError:
# Importar tarea de procesamiento de pedimento (Celery)
from api.customs.tasks.microservice import procesar_pedimento_completo_individual
from api.utils.storage_service import storage_service
def get_available_extractors():
"""
@@ -395,31 +396,8 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
@action(detail=False, methods=['post'], url_path='bulk-delete')
def bulk_delete(self, request):
"""
Endpoint para eliminar múltiples pedimentos de manera masiva.
import traceback
Payload esperado:
{
"ids": ["uuid1", "uuid2", "uuid3", ...]
}
Respuesta exitosa:
{
"message": "Pedimentos eliminados exitosamente",
"deleted_count": 3,
"deleted_ids": ["uuid1", "uuid2", "uuid3"]
}
Respuesta con errores:
{
"message": "Algunos pedimentos no pudieron ser eliminados",
"deleted_count": 2,
"deleted_ids": ["uuid1", "uuid2"],
"failed_ids": ["uuid3"],
"errors": ["No se encontró el pedimento con ID uuid3"]
}
"""
# Obtener los IDs del payload
ids = request.data.get('ids', [])
if not ids:
@@ -434,18 +412,11 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
status=status.HTTP_400_BAD_REQUEST
)
# Obtener el queryset filtrado por organización
queryset = self.get_queryset()
# Filtrar solo los pedimentos que existen y pertenecen a la organización del usuario
existing_pedimentos = queryset.filter(id__in=ids)
existing_ids = list(existing_pedimentos.values_list('id', flat=True))
# Convertir UUIDs a strings para comparación
existing_ids_str = [str(id) for id in existing_ids]
requested_ids_str = [str(id) for id in ids]
# Identificar IDs que no existen o no pertenecen a la organización
failed_ids = [id for id in requested_ids_str if id not in existing_ids_str]
deleted_count = 0
@@ -453,20 +424,28 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
if existing_pedimentos.exists():
try:
# Eliminar los pedimentos encontrados
for pedimento in existing_pedimentos:
documentos = Document.objects.filter(pedimento_id=pedimento.id)
for doc in documentos:
if doc.archivo:
ruta = str(doc.archivo)
try:
storage_service.delete_file(ruta)
except Exception as e:
traceback.print_exc()
documentos.delete()
deleted_count = existing_pedimentos.count()
existing_pedimentos.delete()
except Exception as e:
traceback.print_exc()
return Response(
{"error": f"Error al eliminar pedimentos: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Agregar errores para IDs no encontrados
if failed_ids:
errors = [f"No se encontró el pedimento con ID {id} o no pertenece a su organización" for id in failed_ids]
# Preparar respuesta
response_data = {
"deleted_count": deleted_count,
"deleted_ids": existing_ids_str
@@ -793,14 +772,14 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
print(f"Procesando documento: {file_name}")
try:
# Leer el archivo desde el directorio temporal
# Leer el archivo para extraer info del XML
with open(file_path, 'rb') as f:
file_content = f.read()
from api.utils.helpers import extraer_info_pedimento_xml
# Extraer info del pedimento desde XML si es aplicable
if file_name.lower().endswith('.xml'):
try:
from api.utils.helpers import extraer_info_pedimento_xml
xml_info = extraer_info_pedimento_xml(file_content)
if xml_info:
if 'numero_operacion' in xml_info:
@@ -812,11 +791,12 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
print(f"Información extraída del XML: {xml_info}")
except Exception as e:
print(f"No se pudo extraer información del XML {file_name}: {str(e)}")
# Obtener información del archivo
extension = os.path.splitext(file_name)[1].lower().lstrip('.')
file_size = os.path.getsize(file_path)
# Buscar si ya existe un documento con el mismo nombre para este pedimento
# Buscar si ya existe un documento con el mismo nombre
existing_documents = Document.objects.filter(
pedimento_id=pedimento.id,
organizacion=organizacion
@@ -829,25 +809,30 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
print(f"✅ Encontrado documento existente: ID {doc.id}")
break
# Crear ContentFile
django_file = ContentFile(file_content, name=file_name)
if existing_document:
# Opcional: Eliminar el archivo físico anterior
try:
if existing_document.archivo and os.path.exists(existing_document.archivo.path):
os.remove(existing_document.archivo.path)
except (ValueError, OSError) as e:
print(f"No se pudo eliminar archivo físico anterior: {str(e)}")
# Eliminar archivo anterior si existe
if existing_document.archivo:
storage_service.delete_file(existing_document.archivo)
# Actualizar el documento existente
existing_document.archivo = django_file
existing_document.size = len(file_content)
existing_document.extension = extension
existing_document.updated_at = timezone.now() # Si tienes este campo
existing_document.save()
documents_created += 1
print(f"📄 Documento actualizado: {file_name}")
# Guardar nuevo archivo usando la ruta del archivo temporal
ruta = storage_service.save_document_from_path(
file_path=file_path,
file_name=file_name,
organizacion_id=organizacion.id,
pedimento_app=pedimento_app,
metadata={
'pedimento_id': str(pedimento.id),
'document_id': str(existing_document.id),
'source': 'bulk_create_update'
}
)
if ruta:
existing_document.archivo = ruta
existing_document.size = file_size
existing_document.extension = extension
existing_document.save()
documents_created += 1
else:
# Crear nuevo documento
@@ -856,16 +841,32 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
pedimento_id=pedimento.id,
document_type=document_type,
fuente_id=4,
archivo=django_file,
size=len(file_content),
size=file_size,
extension=extension
)
documents_created += 1
print(f"📄 Nuevo documento creado: {file_name}")
# Guardar archivo usando la ruta del archivo temporal
ruta = storage_service.save_document_from_path(
file_path=file_path,
file_name=file_name,
organizacion_id=organizacion.id,
pedimento_app=pedimento_app,
metadata={
'pedimento_id': str(pedimento.id),
'document_id': str(document.id),
'source': 'bulk_create'
}
)
if ruta:
document.archivo = ruta
document.save()
documents_created += 1
else:
document.delete()
except Exception as e:
print(f"❌ Error al procesar documento {file_name}: {str(e)}")
# Continuar con otros documentos
print(f"🏁 Procesamiento completado. Archivos procesados en este directorio.")
@@ -1359,8 +1360,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# print(f"🔄 Iniciando creación de documento para pedimento ID: {pedimento.id}")
# Crear documento asociado al pedimento
try:
# print("📖 Leyendo archivo desde directorio temporal...")
# Leer el archivo desde el directorio temporal
# Leer el archivo desde el directorio temporal (solo para XML/nomenclatura especial)
with open(file_path, 'rb') as f:
file_content = f.read()
@@ -1372,54 +1372,66 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Patrón: 7 dígitos, punto, 3 dígitos (ej: M8988852.300)
patron_nomenclatura = re.compile(r'^[m|M]\d{7}\.\d{3}$', re.IGNORECASE)
# Separar nombre base y extensión
nombre_base, extension = os.path.splitext(file_name)
if patron_nomenclatura.match(file_name_lower):
tiene_nomenclatura_especial = True
# Procesar el archivo con el método auxiliar
info_extraida = procesar_archivo_m_con_nomenclatura(file_content, pedimento )
# Procesar el archivo con el método auxiliar
info_extraida = procesar_archivo_m_con_nomenclatura(file_content, pedimento)
if info_extraida.get('tiene_nomenclatura_especial', False):
# Agregar información de procesamiento a los datos de respuesta
if 'procesamiento_archivos' not in locals():
procesamiento_archivos = []
if 'procesamiento_archivos' not in locals():
procesamiento_archivos = []
procesamiento_archivos.append({
'archivo': file_name,
'nomenclatura_especial': True,
'registros_encontrados': info_extraida.get('registros_encontrados', []),
'actualizaciones': info_extraida.get('actualizaciones_aplicadas', [])
})
procesamiento_archivos.append({
'archivo': file_name,
'nomenclatura_especial': True,
'registros_encontrados': info_extraida.get('registros_encontrados', []),
'actualizaciones': info_extraida.get('actualizaciones_aplicadas', [])
})
# print(f"📄 Archivo leído: {len(file_content)} bytes")
# Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name)
extension = os.path.splitext(file_name)[1].lower().lstrip('.')
file_size = os.path.getsize(file_path)
fuente, created = Fuente.objects.get_or_create(
nombre="APP-EFC",
descripcion='Transmitido por la app de escritorio'
fuente, created = Fuente.objects.get_or_create(
nombre="APP-EFC",
descripcion='Transmitido por la app de escritorio'
)
# print(f"Creando documento para archivo: {file_name}")
# Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento.id,
document_type=document_type,
fuente_id=fuente.id,
archivo=django_file,
size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.')
size=file_size,
extension=extension
)
# print(f"Documento creado exitosamente: {document.id}")
documents_created += 1
# print(f"📊 Total documentos creados hasta ahora: {documents_created}")
ruta = storage_service.save_document_from_path(
file_path=file_path,
file_name=file_name,
organizacion_id=organizacion.id,
pedimento_app=pedimento_app,
metadata={
'pedimento_id': str(pedimento.id),
'document_id': str(document.id),
'source': 'efc_app_desk',
'tiene_nomenclatura_especial': str(tiene_nomenclatura_especial)
}
)
if ruta:
document.archivo = ruta
document.save()
documents_created += 1
else:
document.delete()
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": "Error al guardar archivo en storage"
})
continue
except Exception as e:
# print(f"❌ Error al crear documento: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
@@ -1817,32 +1829,18 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Crear documento asociado al pedimento
try:
# Leer el archivo desde el directorio temporal
with open(file_path, 'rb') as f:
file_content = f.read()
extension = os.path.splitext(file_name)[1].lower().lstrip('.')
file_size = os.path.getsize(file_path)
# Verificar si el archivo tiene la nomenclatura especial M8988852.300
file_name_lower = file_name.lower()
tiene_nomenclatura_especial = False
info_extraida = {}
# Patrón: 7 dígitos, punto, 3 dígitos (ej: M8988852.300)
patron_nomenclatura = re.compile(r'^[m|M]\d{7}\.\d{3}$', re.IGNORECASE)
# Separar nombre base y extensión
nombre_base, extension = os.path.splitext(file_name)
if patron_nomenclatura.match(file_name_lower):
tiene_nomenclatura_especial = True
# Procesar el archivo con el método auxiliar
with open(file_path, 'rb') as f:
file_content = f.read()
info_extraida = procesar_archivo_m_con_nomenclatura(file_content, pedimento)
if info_extraida.get('tiene_nomenclatura_especial', False):
# Agregar información de procesamiento a los datos de respuesta
if 'procesamiento_archivos' not in locals():
procesamiento_archivos = []
procesamiento_archivos.append({
'archivo': file_name,
'nomenclatura_especial': True,
@@ -1850,20 +1848,15 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
'actualizaciones': info_extraida.get('actualizaciones_aplicadas', [])
})
# Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name)
fuente, created = Fuente.objects.get_or_create(
fuente, _ = Fuente.objects.get_or_create(
nombre="APP-EFC",
descripcion='Transmitido por la app de escritorio'
)
# Buscar si ya existe un documento con el mismo nombre para este pedimento
existing_documents = Document.objects.filter(
pedimento_id=pedimento.id,
organizacion=organizacion
)
existing_document = None
for doc in existing_documents:
if is_same_document(doc, file_name):
@@ -1871,37 +1864,49 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
break
if existing_document:
# Opcional: Eliminar el archivo físico anterior
try:
if existing_document.archivo and os.path.exists(existing_document.archivo.path):
os.remove(existing_document.archivo.path)
except (ValueError, OSError) as e:
pass
# Actualizar el documento existente con el nuevo archivo y datos
existing_document.archivo = django_file
existing_document.size = len(file_content)
existing_document.extension = extension
existing_document.updated_at = timezone.now() # Si tienes este campo
existing_document.save()
documents_created += 1
if existing_document.archivo:
storage_service.delete_file(existing_document.archivo)
ruta = storage_service.save_document_from_path(
file_path=file_path,
file_name=file_name,
organizacion_id=organizacion.id,
pedimento_app=pedimento_app
)
if ruta:
existing_document.archivo = ruta
existing_document.size = file_size
existing_document.extension = extension
existing_document.save()
documents_created += 1
else:
# Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento.id,
document_type=document_type,
fuente_id=fuente.id,
archivo=django_file,
size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.')
size=file_size,
extension=extension
)
documents_created += 1
ruta = storage_service.save_document_from_path(
file_path=file_path,
file_name=file_name,
organizacion_id=organizacion.id,
pedimento_app=pedimento_app
)
if ruta:
document.archivo = ruta
document.save()
documents_created += 1
else:
document.delete()
raise Exception("Error al guardar archivo")
except Exception as e:
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
archivo_original = folder_name + '.zip'
failed_records.append({
"file": relative_path,
"archivo_original": archivo_original,

View File

@@ -26,6 +26,49 @@ from api.organization.models import Organizacion
from api.record.models import Document
from .tasks.auditoria import auditar_pedimento_por_id
from .tasks.auditoria_xml import extraer_info_pedimento_xml
import tempfile
import os
from api.utils.storage_service import storage_service
def get_document_content(documento):
"""
Obtiene el contenido de un documento (MinIO o local).
Retorna el contenido como string o bytes.
"""
ruta = str(documento.archivo)
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp_path = tmp.name
try:
success = storage_service.download_file(ruta, tmp_path)
if not success:
return None
with open(tmp_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
return content
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
def get_document_path(documento):
"""
Obtiene la ruta temporal de un documento para lectura.
Retorna la ruta del archivo temporal descargado.
"""
ruta = str(documento.archivo)
tmp = tempfile.NamedTemporaryFile(delete=False)
tmp_path = tmp.name
tmp.close()
success = storage_service.download_file(ruta, tmp_path)
if not success:
return None
return tmp_path
@swagger_auto_schema(
method='post',
@@ -729,10 +772,10 @@ def auditar_peticion_respuesta_pedimento_completo(request):
for documento in documentos_peticion:
nombre_archivo = os.path.basename(documento.archivo.name)
ruta_temporal = get_document_path(documento)
documentos_lista_peticiones.append({
'id': str(documento.id),
'archivo': documento.archivo.path,
'archivo': ruta_temporal,
'archivo_original': nombre_archivo,
'extension': documento.extension,
'size': documento.size,
@@ -1623,36 +1666,32 @@ def auditar_pedimento_endpoint(request):
try:
xml_info = {
'documento_id': str(documento.id),
'nombre_archivo': os.path.basename(documento.archivo.name),
'nombre_archivo': os.path.basename(str(documento.archivo)),
'tamanio': documento.size,
'extension': documento.extension,
'tipo_documento': documento.document_type.descripcion if documento.document_type else 'Desconocido'
}
# Intentar extraer información del XML
try:
with open(documento.archivo.path, 'r', encoding='utf-8') as xml_file:
xml_content = xml_file.read()
# Extraer información específica del XML
xml_content = get_document_content(documento)
if xml_content is None:
xml_info['error_lectura'] = 'No se pudo descargar el archivo'
else:
info_pedimento = extraer_info_pedimento_xml(xml_content)
if info_pedimento:
xml_info['informacion_extraida'] = info_pedimento
informacion_extraida.append(info_pedimento)
# Actualizar el pedimento con la información encontrada si es necesario
actualizar_info_pedimento(pedimento, info_pedimento)
except Exception as e:
xml_info['error_lectura'] = str(e)
xmls_analizados.append(xml_info)
except Exception as e:
xmls_analizados.append({
'documento_id': str(documento.id),
'nombre_archivo': os.path.basename(documento.archivo.name),
'nombre_archivo': os.path.basename(str(documento.archivo)),
'error': f'Error procesando archivo: {str(e)}'
})