1 Commits

14 changed files with 1832 additions and 1281 deletions

View File

@@ -1,3 +1,6 @@
import os
from datetime import datetime
from django.db import models
from celery import shared_task, group
from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument
from core.utils import xml_controller
@@ -198,7 +201,7 @@ def auditar_coves(organizacion_id):
pedimento,
servicio=8,
related_name='coves',
variable='acuse_descargado',
variable='cove_descargado',
mensaje='COVE'
)
@@ -247,7 +250,7 @@ def auditar_cove_por_pedimento(pedimento_id):
pedimento,
servicio=8,
related_name='coves',
variable='acuse_descargado',
variable='cove_descargado',
mensaje='COVE'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
@@ -302,3 +305,154 @@ def auditar_acuse_por_pedimento(pedimento_id):
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_pedimento_por_id(pedimento_id):
"""
Tarea para auditar un pedimento específico verificando todos sus documentos y datos.
"""
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
resultado = {
'pedimento_id': str(pedimento_id),
'pedimento': pedimento.pedimento,
'pedimento_app': pedimento.pedimento_app,
'organizacion': str(pedimento.organizacion.id),
'fecha_auditoria': datetime.now().isoformat(),
'estado_general': 'EN_PROGRESO',
'detalles': {}
}
# 1. Verificar documentos XML
from api.record.models import Document
documentos_xml = Document.objects.filter(
pedimento=pedimento,
archivo__endswith='.xml'
)
resultado['detalles']['documentos_xml'] = {
'total': documentos_xml.count(),
'archivos': []
}
for doc in documentos_xml:
try:
xml_info = {
'id': str(doc.id),
'nombre': os.path.basename(doc.archivo.name),
'tamanio': doc.size,
'extension': doc.extension,
'tipo': doc.document_type.descripcion if doc.document_type else 'Desconocido'
}
# Verificar si el archivo existe físicamente
if os.path.exists(doc.archivo.path):
xml_info['existe_fisicamente'] = True
# Intentar leer el XML
try:
with open(doc.archivo.path, 'r', encoding='utf-8') as f:
content = f.read()
xml_info['es_xml_valido'] = '<?xml' in content[:100]
xml_info['tamanio_bytes'] = len(content)
except Exception as e:
xml_info['error_lectura'] = str(e)
else:
xml_info['existe_fisicamente'] = False
except Exception as e:
xml_info['error'] = str(e)
resultado['detalles']['documentos_xml']['archivos'].append(xml_info)
# 2. Verificar si hay documentos asociados
resultado['detalles']['documentos_totales'] = {
'total': pedimento.documents.count(),
'por_tipo': {}
}
for doc_type in pedimento.documents.values('document_type__descripcion').annotate(total=models.Count('id')):
tipo = doc_type['document_type__descripcion'] or 'Sin tipo'
resultado['detalles']['documentos_totales']['por_tipo'][tipo] = doc_type['total']
# 3. Verificar COVEs
resultado['detalles']['coves'] = {
'total': pedimento.coves.count(),
'descargados': pedimento.coves.filter(cove_descargado=True).count(),
'con_acuse': pedimento.coves.filter(acuse_cove_descargado=True).count()
}
# 4. Verificar EDocuments
resultado['detalles']['edocuments'] = {
'total': pedimento.documentos.count(),
'descargados': pedimento.documentos.filter(edocument_descargado=True).count(),
'con_acuse': pedimento.documentos.filter(acuse_descargado=True).count()
}
# 5. Verificar procesamientos
resultado['detalles']['procesamientos'] = {
'total': pedimento.procesamientos.count(),
'por_estado': {}
}
for proc in pedimento.procesamientos.values('estado__estado').annotate(total=models.Count('id')):
estado = proc['estado__estado'] or 'Sin estado'
resultado['detalles']['procesamientos']['por_estado'][estado] = proc['total']
# 6. Verificar campos importantes del pedimento
campos_revisados = {
'numero_operacion': bool(pedimento.numero_operacion),
'numero_partidas': bool(pedimento.numero_partidas),
'importe_total': bool(pedimento.importe_total),
'contribuyente': bool(pedimento.contribuyente),
'tiene_remesas': pedimento.remesas,
'partidas_creadas': pedimento.partidas.count() > 0,
'fecha_pago': bool(pedimento.fecha_pago)
}
resultado['detalles']['campos_pedimento'] = campos_revisados
resultado['detalles']['campos_completos'] = sum(campos_revisados.values())
resultado['detalles']['campos_totales'] = len(campos_revisados)
# 7. Determinar estado general
campos_completos = resultado['detalles']['campos_completos']
total_campos = resultado['detalles']['campos_totales']
if documentos_xml.count() == 0:
resultado['estado_general'] = 'SIN_XML'
resultado['mensaje'] = 'No se encontraron documentos XML'
elif campos_completos == total_campos:
resultado['estado_general'] = 'COMPLETO'
resultado['mensaje'] = 'Pedimento completamente procesado'
elif campos_completos >= total_campos * 0.7:
resultado['estado_general'] = 'PARCIAL'
resultado['mensaje'] = 'Pedimento parcialmente procesado'
else:
resultado['estado_general'] = 'INCOMPLETO'
resultado['mensaje'] = 'Pedimento con información incompleta'
resultado['porcentaje_completitud'] = (campos_completos / total_campos) * 100 if total_campos > 0 else 0
# 8. Sugerencias
sugerencias = []
if not pedimento.numero_operacion:
sugerencias.append("Falta el número de operación")
if not pedimento.numero_partidas:
sugerencias.append("Falta el número de partidas")
if pedimento.numero_partidas and pedimento.numero_partidas > pedimento.partidas.count():
sugerencias.append(f"Faltan partidas: {pedimento.numero_partidas - pedimento.partidas.count()} de {pedimento.numero_partidas}")
if not pedimento.contribuyente:
sugerencias.append("Falta el contribuyente asociado")
resultado['sugerencias'] = sugerencias
return resultado
except Pedimento.DoesNotExist:
return {
'error': f'Pedimento con ID {pedimento_id} no encontrado',
'pedimento_id': str(pedimento_id)
}
except Exception as e:
return {
'error': f'Error auditar pedimento {pedimento_id}: {str(e)}',
'pedimento_id': str(pedimento_id)
}

View File

@@ -0,0 +1,194 @@
# auditoria_xml.py
import xml.etree.ElementTree as ET
from datetime import datetime
def extraer_info_pedimento_xml(xml_content):
"""
Extrae información específica de un XML de pedimento.
"""
try:
# Parsear el XML
root = ET.fromstring(xml_content)
# Buscar el namespace (puede variar)
namespaces = {
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
'ns3': 'http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta'
}
resultado = {}
# Extraer número de operación
num_op = root.find('.//ns2:numeroOperacion', namespaces)
if num_op is not None and num_op.text:
resultado['numero_operacion'] = num_op.text
# Extraer información del pedimento
pedimento_elem = root.find('.//ns2:pedimento', namespaces)
if pedimento_elem is not None:
# Número de pedimento
ped_num = pedimento_elem.find('ns2:pedimento', namespaces)
if ped_num is not None and ped_num.text:
resultado['numero_pedimento'] = ped_num.text
# Número de partidas
partidas = pedimento_elem.find('ns2:partidas', namespaces)
if partidas is not None and partidas.text:
try:
resultado['numero_partidas'] = int(partidas.text)
except (ValueError, TypeError):
pass
# Tipo de operación clave
tipo_op_clave = pedimento_elem.find('.//ns2:tipoOperacion/ns2:clave', namespaces)
if tipo_op_clave is not None and tipo_op_clave.text:
if tipo_op_clave.text.strip() == '1':
resultado['tipo_operacion'] = 'Importacion'
resultado['tipo_operacion_descripcion'] = 'Indica operacion como Importaciones'
elif tipo_op_clave.text.strip() == '2':
resultado['tipo_operacion'] = 'Exportacion'
resultado['tipo_operacion_descripcion'] = 'Indica operacion de exportacion'
# Clave del documento (clave_pedimento)
clave_doc = pedimento_elem.find('.//ns2:claveDocumento/ns2:clave', namespaces)
if clave_doc is not None and clave_doc.text:
resultado['clave_pedimento'] = clave_doc.text.strip()
# Aduana (patente)
aduana = pedimento_elem.find('.//ns2:aduanaEntradaSalida/ns2:clave', namespaces)
if aduana is not None and aduana.text:
resultado['aduana_clave'] = aduana.text.strip()
# Importador/Exportador
importador = pedimento_elem.find('.//ns2:importadorExportador', namespaces)
if importador is not None:
rfc = importador.find('ns2:rfc', namespaces)
if rfc is not None and rfc.text:
resultado['contribuyente_rfc'] = rfc.text.strip()
razon_social = importador.find('ns2:razonSocial', namespaces)
if razon_social is not None and razon_social.text:
resultado['contribuyente_nombre'] = razon_social.text.strip()
# Valor en dólares
valor_dolares = importador.find('ns2:valorDolares', namespaces)
if valor_dolares is not None and valor_dolares.text:
try:
resultado['valor_dolares'] = float(valor_dolares.text)
except (ValueError, TypeError):
pass
# Aduana de despacho
aduana_despacho = importador.find('ns2:aaduanaDespacho/ns2:clave', namespaces)
if aduana_despacho is not None and aduana_despacho.text:
resultado['aduana_despacho'] = aduana_despacho.text.strip()
# Encabezado del pedimento
encabezado = pedimento_elem.find('ns2:encabezado', namespaces)
if encabezado is not None:
# Aduana
aduana = encabezado.find('ns2:aduanaEntradaSalida/ns2:clave', namespaces)
if aduana is not None and aduana.text:
resultado['aduana_clave'] = aduana.text.strip()
# Tipo de cambio
tipo_cambio = encabezado.find('ns2:tipoCambio', namespaces)
if tipo_cambio is not None and tipo_cambio.text:
try:
resultado['tipo_cambio'] = float(tipo_cambio.text)
except (ValueError, TypeError):
pass
# RFC Agente Aduanal
rfc_agente = encabezado.find('ns2:rfcAgenteAduanalSocFactura', namespaces)
if rfc_agente is not None and rfc_agente.text:
resultado['rfc_agente_aduanal'] = rfc_agente.text.strip()
# CURP Apoderado
curp_apoderado = encabezado.find('ns2:curpApoderadomandatario', namespaces)
if curp_apoderado is not None and curp_apoderado.text:
resultado['curp_apoderado'] = curp_apoderado.text.strip()
# Valor Aduanal Total
valor_aduanal = encabezado.find('ns2:valorAduanalTotal', namespaces)
if valor_aduanal is not None and valor_aduanal.text:
try:
resultado['valor_aduanal_total'] = float(valor_aduanal.text)
except (ValueError, TypeError):
pass
# Valor Comercial Total
valor_comercial = encabezado.find('ns2:valorComercialTotal', namespaces)
if valor_comercial is not None and valor_comercial.text:
try:
resultado['valor_comercial_total'] = float(valor_comercial.text)
except (ValueError, TypeError):
pass
# Fechas
fechas = pedimento_elem.findall('.//ns2:fechas', namespaces)
for fecha_elem in fechas:
fecha = fecha_elem.find('ns2:fecha', namespaces)
clave_fecha = fecha_elem.find('ns2:tipo/ns2:clave', namespaces)
if fecha is not None and fecha.text and clave_fecha is not None and clave_fecha.text:
fecha_texto = fecha.text.strip()
clave_fecha_texto = clave_fecha.text.strip()
# Mapeo de claves según especificación
if clave_fecha_texto == '1': # Entrada
resultado['fecha_entrada'] = fecha_texto
elif clave_fecha_texto == '2': # Pago
resultado['fecha_pago'] = fecha_texto
elif clave_fecha_texto == '3': # Extracción
resultado['fecha_extraccion'] = fecha_texto
elif clave_fecha_texto == '5': # Presentación
resultado['fecha_presentacion'] = fecha_texto
elif clave_fecha_texto == '6': # Importación
resultado['fecha_importacion'] = fecha_texto
elif clave_fecha_texto == '7': # Original
resultado['fecha_original'] = fecha_texto
else:
resultado[f'fecha_clave_{clave_fecha_texto}'] = fecha_texto
# Facturas (para COVEs)
facturas = pedimento_elem.findall('.//ns2:facturas', namespaces)
coves_encontrados = []
for factura in facturas:
numero = factura.find('ns2:numero', namespaces)
if numero is not None and numero.text:
coves_encontrados.append(numero.text.strip())
if coves_encontrados:
resultado['coves_en_xml'] = coves_encontrados
# E-Documents
identificadores = pedimento_elem.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
edocs_encontrados = []
for ident in identificadores:
clave = ident.find('claveIdentificador/descripcion', namespaces)
complemento = ident.find('complemento1', namespaces)
if clave is not None and clave.text and 'E_DOCUMENT' in clave.text:
if complemento is not None and complemento.text:
edocs_encontrados.append(complemento.text.strip())
if edocs_encontrados:
resultado['edocuments_en_xml'] = edocs_encontrados
# Verificar si hay error en la respuesta
tiene_error = root.find('.//ns3:tieneError', namespaces)
if tiene_error is not None:
resultado['tiene_error'] = tiene_error.text.lower() == 'true'
return resultado
except ET.ParseError as e:
return {'error_parse': str(e)}
except Exception as e:
return {'error': str(e)}

View File

@@ -42,7 +42,24 @@ from .views_auditor import (
auditar_acuse_cove_pedimento_endpoint,
auditar_edocument_pedimento_endpoint,
auditar_acuse_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion
auditar_procesamiento_remesa_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion,
auditar_peticion_respuesta_pedimento_completo,
auditor_obtener_peticion_pedimento_vu,
auditor_obtener_respuesta_pedimento_vu,
auditor_obtener_peticion_remesa_vu,
auditor_obtener_respuesta_remesa_vu,
auditor_obtener_peticion_partidas_vu,
auditor_obtener_respuesta_partidas_vu,
auditor_obtener_peticion_acuse_vu,
auditor_obtener_respuesta_acuse_vu,
auditor_obtener_peticion_cove_vu,
auditor_obtener_respuesta_cove_vu,
auditor_obtener_peticion_acuse_cove_vu,
auditor_obtener_respuesta_acuse_cove_vu,
auditor_obtener_peticion_edocument_vu,
auditor_obtener_respuesta_edocument_vu,
auditar_pedimento_endpoint,
)
urlpatterns = [
@@ -58,5 +75,24 @@ urlpatterns = [
path('auditor/auditar-acuse-cove/pedimento/', auditar_acuse_cove_pedimento_endpoint, name='auditar-acuse-cove-pedimento'),
path('auditor/auditar-edocument/pedimento/', auditar_edocument_pedimento_endpoint, name='auditar-edocument-pedimento'),
path('auditor/auditar-acuse/pedimento/', auditar_acuse_pedimento_endpoint, name='auditar-acuse-pedimento'),
path('auditor/auditar-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-remesa-pedimento'),
path('auditor/auditar-pedimento/', auditar_pedimento_endpoint, name='auditar-pedimento'),
path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'),
path('auditor/peticion-respuesta/pedimento-vu/', auditar_peticion_respuesta_pedimento_completo, name='peticion-respuesta-pedimento-vu'),
path('auditor/obtener-peticion/pedimento-vu/', auditor_obtener_peticion_pedimento_vu, name='obtener-peticion-pedimento-vu'),
path('auditor/obtener-respuesta/pedimento-vu/', auditor_obtener_respuesta_pedimento_vu, name='obtener-respuesta-pedimento-vu'),
path('auditor/obtener-peticion/remesa-vu/', auditor_obtener_peticion_remesa_vu, name='obtener-peticion-remesa-vu'),
path('auditor/obtener-respuesta/remesa-vu/', auditor_obtener_respuesta_remesa_vu, name='obtener-respuesta-remesa-vu'),
path('auditor/obtener-peticion/partidas-vu/', auditor_obtener_peticion_partidas_vu, name='obtener-peticion-partidas-vu'),
path('auditor/obtener-respuesta/partidas-vu/', auditor_obtener_respuesta_partidas_vu, name='obtener-respuesta-partidas-vu'),
path('auditor/obtener-peticion/acuse-vu/', auditor_obtener_peticion_acuse_vu, name='obtener-peticion-acuse-vu'),
path('auditor/obtener-respuesta/acuse-vu/', auditor_obtener_respuesta_acuse_vu, name='obtener-respuesta-acuse-vu'),
path('auditor/obtener-peticion/cove-vu/', auditor_obtener_peticion_cove_vu, name='obtener-peticion-cove-vu'),
path('auditor/obtener-respuesta/cove-vu/', auditor_obtener_respuesta_cove_vu, name='obtener-respuesta-cove-vu'),
path('auditor/obtener-peticion/acuse-cove-vu/', auditor_obtener_peticion_acuse_cove_vu, name='obtener-peticion-acuse-cove-vu'),
path('auditor/obtener-respuesta/acuse-cove-vu/', auditor_obtener_respuesta_acuse_cove_vu, name='obtener-respuesta-acuse-cove-vu'),
path('auditor/obtener-peticion/edocument-vu/', auditor_obtener_peticion_edocument_vu, name='obtener-peticion-edocument-vu'),
path('auditor/obtener-respuesta/edocument-vu/', auditor_obtener_respuesta_edocument_vu, name='obtener-respuesta-edocument-vu'),
]

View File

@@ -49,9 +49,7 @@ from django.core.files.base import ContentFile
from django.db import transaction
from rest_framework.parsers import MultiPartParser, FormParser
from api.record.models import Document, DocumentType, Fuente
from unicodedata import normalize
from datetime import datetime
from django.utils import timezone
# Importar rarfile de manera opcional
try:
import rarfile
@@ -59,6 +57,9 @@ try:
except ImportError:
RAR_SUPPORT = False
# Importar tarea de procesamiento de pedimento (Celery)
from api.customs.tasks.microservice import procesar_pedimento_completo_individual
def get_available_extractors():
"""
Devuelve lista de extractores disponibles en orden de preferencia
@@ -371,6 +372,25 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
]
}
@action(detail=True, methods=['post'], url_path='procesar-completo')
def procesar_completo(self, request, pk=None):
"""
Acción para disparar el procesamiento completo de un pedimento existente.
Dispara la tarea `procesar_pedimento_completo_individual` de forma asíncrona
y devuelve el `task_id`.
"""
pedimento = self.get_object()
try:
# Usar el nombre del servicio de Docker Compose en lugar de localhost
task = procesar_pedimento_completo_individual.delay(pedimento.id, pedimento.organizacion.id)
# Verificar si la respuesta fue exitosa
if task.id:
return Response({"status": "Recurso creado exitosamente en API", "task_id": task.id}, status=status.HTTP_202_ACCEPTED)
else:
return Response({"status": "Servicio API respondió con error", "task_id": 0}, status=status.HTTP_202_ACCEPTED)
except Exception as e:
return Response({"error": f"Error inesperado al llamar al servicio API: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'], url_path='bulk-delete')
def bulk_delete(self, request):
"""
@@ -800,61 +820,56 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
})
continue
else:
print(f"♻️ Usando pedimento existente: ID {existing_pedimento.id}")
# Usar pedimento existente
pedimento = existing_pedimento
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
with open(file_path, 'rb') as f:
file_content = f.read()
# Obtener información del archivo
extension = os.path.splitext(file_name)[1].lower().lstrip('.')
# Buscar todos los documentos existentes para este pedimento
existing_documents = Document.objects.filter(
pedimento_id=pedimento.id,
organizacion=organizacion
)
# Buscar si ya existe un documento con el mismo nombre base
existing_document = None
for doc in existing_documents:
if is_same_document(doc, file_name):
existing_document = doc
print(f"✅ Encontrado documento existente: ID {doc.id}")
break
# Crear ContentFile
print(f"📄 Archivo leído: {len(file_content)} bytes")
# Crear ContentFile que Django puede manejar correctamente
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)}")
# 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()
else:
# Crear nuevo documento
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento.id,
document_type=document_type,
fuente_id=4,
archivo=django_file,
size=len(file_content),
extension=extension
)
# # Verificar si el documento ya existe para este pedimento y archivo
# print("🔍 Verificando existencia previa del documento...")
# # Reemplazar múltiples caracteres
# normalized_file_name = file_name.replace(" ", "_")
# file_name_without_extension = normalized_file_name.rsplit('.', 1)[0]
# extension_file = os.path.splitext(normalized_file_name)[1].lower().lstrip('.')
# existing_document = Document.objects.filter(
# pedimento_id=pedimento.id,
# archivo__contains=file_name_without_extension,
# extension=extension_file
# ).first()
# if existing_document:
# print(f"Documento existente encontrado, omitiendo creación: ID {existing_document.id}")
# continue
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=4, # Fuente: Carga Plataforma
archivo=django_file,
size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.')
)
print(f"Documento creado exitosamente: {document.id}")
documents_created += 1
print(f"📊 Total documentos creados hasta ahora: {documents_created}")
except Exception as e:
print(f"❌ Error al crear documento: {str(e)}")
@@ -1757,85 +1772,4 @@ class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador")
my_tags = ['Importadores']
# helper | reglas para formato de docuemnto antes de cargarlo
def normalize_filename(filename):
"""
Normaliza el nombre del archivo removiendo caracteres especiales,
espacios y asegurando consistencia.
"""
filename = normalize('NFKD', filename).encode('ASCII', 'ignore').decode('ASCII')
filename = re.sub(r'[^\w\s.-]', '_', filename) # Remover caracteres no alfanuméricos
filename = re.sub(r'[\s()]+', '_', filename) # Reemplazar espacios y paréntesis
filename = re.sub(r'_+', '_', filename) # Consolidar múltiples _
filename = filename.strip('_') # Remover _ al inicio/final
return filename
def get_clean_base_filename(filename):
"""
Obtiene el nombre base limpio sin el sufijo de Django.
"""
normalized = normalize_filename(filename)
name_without_ext, ext = os.path.splitext(normalized)
django_suffix = extract_django_suffix(name_without_ext)
if django_suffix:
base_name = name_without_ext[:-8]
else:
base_name = name_without_ext
base_name = re.sub(r'(_copy|_copia|_-_copia|_-_copy)(_\d+)?$', '', base_name)
return base_name.lower().strip('_')
def is_same_document(existing_doc, new_filename):
"""
Compara si un documento existente y un nuevo archivo son el mismo documento.
Args:
existing_doc: Objeto Document existente
new_filename: Nombre del nuevo archivo a subir
Returns:
bool: True si son el mismo documento
"""
existing_basename = os.path.basename(existing_doc.archivo.name)
existing_base = get_clean_base_filename(existing_basename)
new_base = get_clean_base_filename(new_filename)
existing_ext = existing_doc.extension.lower()
new_ext = os.path.splitext(new_filename)[1].lower().lstrip('.')
return existing_base == new_base and existing_ext == new_ext
def extract_django_suffix(filename):
"""
Extrae el sufijo único que Django añade a los archivos.
"""
name_without_ext = os.path.splitext(filename)[0]
match = re.search(r'_([a-zA-Z0-9]{7})$', name_without_ext)
if match:
return match.group(1)
return None
def get_clean_base_filename(filename):
"""
Obtiene el nombre base limpio sin el sufijo de Django.
"""
normalized = normalize_filename(filename)
name_without_ext, ext = os.path.splitext(normalized)
django_suffix = extract_django_suffix(name_without_ext)
if django_suffix:
base_name = name_without_ext[:-8]
else:
base_name = name_without_ext
base_name = re.sub(r'(_copy|_copia|_-_copia|_-_copy)(_\d+)?$', '', base_name)
return base_name.lower().strip('_')
my_tags = ['Importadores']

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +0,0 @@
from celery import shared_task
from django.core.files.base import ContentFile
from django.utils import timezone
from api.reports.models import ReportDocument
from api.customs.models import Pedimento, Cove, EDocument, Partida
from django.db.models import Q
import csv
import os
from django.conf import settings
import logging
logger = logging.getLogger()
@shared_task
def generate_report_document(report_id):
try:
report = ReportDocument.objects.get(id=report_id)
report.status = 'processing'
report.save(update_fields=['status'])
filters = report.filters or {}
pedimentos_filters = Q()
if filters.get('organizacion_id'):
pedimentos_filters &= Q(organizacion_id=filters['organizacion_id'])
if filters.get('fecha_pago__gte'):
pedimentos_filters &= Q(fecha_pago__gte=filters['fecha_pago__gte'])
if filters.get('fecha_pago__lte'):
pedimentos_filters &= Q(fecha_pago__lte=filters['fecha_pago__lte'])
if filters.get('contribuyente__rfc'):
pedimentos_filters &= Q(contribuyente__rfc=filters['contribuyente__rfc'])
if filters.get('patente'):
pedimentos_filters &= Q(patente=filters['patente'])
if filters.get('aduana'):
pedimentos_filters &= Q(aduana=filters['aduana'])
if filters.get('pedimento'):
pedimentos_filters &= Q(pedimento=filters['pedimento'])
if filters.get('pedimento_app'):
pedimentos_filters &= Q(pedimento_app=filters['pedimento_app'])
if filters.get('regimen'):
pedimentos_filters &= Q(regimen=filters['regimen'])
if filters.get('tipo_operacion'):
pedimentos_filters &= Q(tipo_operacion_id=filters['tipo_operacion'])
pedimentos = Pedimento.objects.filter(pedimentos_filters)
filename = filters.get('filename')
if filename:
filename = f"{filename}.csv" if not filename.endswith('.csv') else filename
else:
filename = f"report_{report.id}_{timezone.now().strftime('%Y%m%d%H%M%S')}.csv"
file_path = os.path.join(settings.MEDIA_ROOT, 'reports', filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
headers = [
'aduana', 'patente', 'regimen', 'pedimento', 'pedimento_app', 'clave_pedimento',
'tipo_operacion_id', 'contribuyente_id', 'tipo_documento', 'numero_documento', 'estado', 'acuse_estado'
]
writer.writerow(headers)
for ped in pedimentos:
for cove in Cove.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'COVE', cove.numero_cove, cove.cove_descargado, cove.acuse_cove_descargado
])
for edoc in EDocument.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'EDOC', edoc.numero_edocument, edoc.edocument_descargado, edoc.acuse_descargado
])
for partida in Partida.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'PARTIDA', partida.numero_partida, partida.descargado, ''
])
with open(file_path, 'rb') as f:
report.file.save(filename, ContentFile(f.read()), save=True)
report.status = 'ready'
report.finished_at = timezone.now()
report.save(update_fields=['status', 'file', 'finished_at'])
except Exception as e:
report.status = 'error'
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])

View File

@@ -27,7 +27,7 @@ class ViewSetOrganizacion(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltr
queryset = Organizacion.objects.all()
serializer_class = OrganizacionSerializer
filterset_fields = ['nombre']
filterset_fields = ['nombre', 'descripcion']
my_tags = ['Organizaciones']

View File

@@ -11,7 +11,8 @@ from .views import (DocumentViewSet
, DocumentTypeView
, ExpedienteZipDownloadView
, MultiPedimentoZipDownloadView
, PedimentoDocumentViewSet)
, PedimentoDocumentViewSet
, TriggerPedimentoCompletoView)
# Create a router and register your viewsets with it
@@ -35,5 +36,6 @@ urlpatterns = [
path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'),
path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'),
path('pedimento-documents/', PedimentoDocumentViewSet.as_view({'get': 'list'}), name='pedimento-document-list'),
path('microservice/pedimento-completo/', TriggerPedimentoCompletoView.as_view(), name='trigger-pedimento-completo'),
path('', include(router.urls)),
]

View File

@@ -14,6 +14,7 @@ from rest_framework.exceptions import ValidationError
from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer
from .models import Document, Fuente, DocumentType
from ..customs.models import Pedimento
from ..vucem.models import CredencialesImportador
from api.organization.models import UsoAlmacenamiento
from io import BytesIO
import zipfile
@@ -35,9 +36,91 @@ logger = logging.getLogger(__name__)
import os
from django.core.files.storage import default_storage
from django.conf import settings
import requests
import re
from mixins.filtrado_organizacion import DocumentosFiltradosMixin
# Configuración de patrones y mapeos (definirlo fuera del try para reutilización)
DOCUMENT_PATTERNS = {
'REQUEST': {
'VU_PC': (r".*VU_PC.*REQUEST\.xml$", 13, "Request Pedimento Completo VU"),
'VU_ED': (r".*VU_ED.*REQUEST\.xml$", 21, "Request E-Document VU"),
'VU_PT': (r".*VU_PT.*REQUEST\.xml$", 17, "Request Partidas VU"),
'VU_AC_COVE': (r".*VU_AC_COVE.*REQUEST\.xml$", 23, "Request Acuses COVES VU"),
'VU_COVE': (r".*VU_COVE.*REQUEST\.xml$", 19, "Request COVES VU"),
'VU_RM': (r".*VU_RM.*REQUEST\.xml$", 15, "Request Remesas VU"),
'VU_AC': (r".*VU_AC.*REQUEST\.xml$", 25, "Request Acuses VU"),
},
'ERROR': {
'VU_PC': (r".*VU_PC.*ERROR\.xml$", 14, "Error Pedimento Completo VU"),
'VU_ED': (r".*VU_ED.*ERROR\.xml$", 22, "Error E-Document VU"),
'VU_PT': (r".*VU_PT.*ERROR\.xml$", 18, "Error Partidas VU"),
'VU_AC_COVE': (r".*VU_AC_COVE.*ERROR\.xml$", 24, "Error Acuses COVES VU"),
'VU_COVE': (r".*VU_COVE.*ERROR\.xml$", 20, "Error COVES VU"),
'VU_RM': (r".*VU_RM.*ERROR\.xml$", 16, "Error Remesas VU"),
'VU_AC': (r".*VU_AC.*ERROR\.xml$", 26, "Error Acuses VU"),
}
}
def eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id):
"""Elimina documentos existentes con el mismo nombre base"""
documentos_existentes = Document.objects.filter(
archivo__icontains=nombre_sin_extension,
organizacion=organizacion,
pedimento_id=pedimento_id
)
if not documentos_existentes.exists():
return
for doc_existente in documentos_existentes:
# Eliminar archivo físico si existe
if doc_existente.archivo and os.path.exists(doc_existente.archivo.path):
try:
os.remove(doc_existente.archivo.path)
except Exception as e:
logger.error(f"Error al eliminar archivo físico: {e}")
# Eliminar registros de la base de datos
documentos_existentes.delete()
def obtener_tipo_documento_por_patron(nombre_archivo, organizacion, pedimento_id):
"""Determina el tipo de documento basado en patrones de nombre"""
nombre_sin_extension = nombre_archivo.rsplit('.', 1)[0]
# Verificar patrones REQUEST
for doc_key, (patron, type_id, descripcion) in DOCUMENT_PATTERNS['REQUEST'].items():
if re.search(patron, nombre_archivo, re.IGNORECASE):
try:
# Eliminar documentos existentes
eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id)
# Obtener tipo de documento
return DocumentType.objects.get(id=type_id)
except DocumentType.DoesNotExist:
raise ValidationError({
"error": f"El tipo de documento '{descripcion}' no existe. Por favor, créelo primero."
})
# Verificar patrones ERROR (si es necesario procesarlos)
for doc_key, (patron, type_id, descripcion) in DOCUMENT_PATTERNS['ERROR'].items():
if re.search(patron, nombre_archivo, re.IGNORECASE):
try:
# Eliminar documentos existentes
eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id)
# Obtener tipo de documento
return DocumentType.objects.get(id=type_id)
except DocumentType.DoesNotExist:
raise ValidationError({
"error": f"El tipo de documento '{descripcion}' no existe. Por favor, créelo primero."
})
return None
class CustomPagination(PageNumberPagination):
"""
@@ -144,12 +227,45 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"codigo": "storage_limit_exceeded"
}, code=status.HTTP_400_BAD_REQUEST)
# Guardar documento y actualizar espacio atómicamente
documento = serializer.save(
organizacion=organizacion,
size=archivo.size,
extension=archivo.name.split('.')[-1].lower()
)
try:
pedimento_id = serializer.validated_data.get('pedimento').id if serializer.validated_data.get('pedimento') else None
document_type = serializer.validated_data.get('document_type')
# Determinar el tipo de documento basado en el nombre del archivo
detected_type = obtener_tipo_documento_por_patron(archivo.name, organizacion, pedimento_id)
if detected_type:
document_type = detected_type
else:
# Lógica para archivos que no coinciden con los patrones conocidos
logger.warning(f"No se encontró patrón para archivo: {archivo.name}")
# Puedes mantener el document_type original o manejarlo de otra forma
except ValidationError as ve:
raise ve
except Exception as e:
# Como fallback, intentar obtener cualquier DocumentType existente
logger.error(f"Error al determinar el tipo de documento basado en el nombre del archivo: {e}")
try:
# Guardar documento y actualizar espacio atómicamente
documento = serializer.save(
document_type=document_type,
organizacion=organizacion,
size=archivo.size,
extension=archivo.name.split('.')[-1].lower()
)
except Exception as e:
# Guardar documento y actualizar espacio atómicamente
documento = serializer.save(
organizacion=organizacion,
size=archivo.size,
extension=archivo.name.split('.')[-1].lower()
)
uso.espacio_utilizado = nuevo_espacio_utilizado
uso.save()
@@ -833,7 +949,114 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero)
return queryset
class TriggerPedimentoCompletoView(APIView):
"""
Endpoint interno para disparar la descarga de pedimento completo
en el microservicio FastAPI. Reenvía el payload tal cual y devuelve
la respuesta del microservicio (normalmente un `task_id`).
"""
# permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
my_tags = ['Microservice - Pedimento Completo']
def post(self, request):
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
return Response({"error": "Usuario no autenticado o sin organización"}, status=401)
# Validación mínima
# if not payload.get('credencial') or not payload.get('pedimento_id'):
# return Response({"error": "Se requieren 'credencial' y 'pedimento'"}, status=status.HTTP_400_BAD_REQUEST)
if not request.data.get('pedimento_id'):
return Response({"error": "Se requieren 'credencial' y 'pedimento'"}, status=status.HTTP_400_BAD_REQUEST)
pedimento_id = request.data.get('pedimento_id')
# Verificar que el pedimento existe y pertenece a la organización del usuario
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
if not pedimento.contribuyente:
return Response({"error": "El pedimento no tiene un contribuyente asociado"}, status=status.HTTP_400_BAD_REQUEST)
contribuyente_rfc = pedimento.contribuyente.rfc
payload = {
"pedimento": {
"id": str(pedimento.id),
"pedimento": pedimento.pedimento,
"pedimento_app": pedimento.pedimento_app,
"aduana": pedimento.aduana,
"patente": pedimento.patente,
"contribuyente": contribuyente_rfc,
"organizacion": str(pedimento.organizacion.id)
},
"credencial": {
"id": "",
"user": "",
"password": "",
"efirma": "",
"key": "",
"cer": "",
"is_active": False,
"organizacion": ""
}
}
except Pedimento.DoesNotExist:
return Response({"error": "Pedimento no encontrado"}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({"error": f"Error al buscar pedimento: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
try:
credenciales = CredencialesImportador.objects.get(rfc=contribuyente_rfc)
vucem = credenciales.vucem
# Obtener las rutas de los archivos, no los objetos FieldFile
key_path = vucem.key.path if vucem.key else ""
cer_path = vucem.cer.path if vucem.cer else ""
payload['credencial'] = {
"id": str(credenciales.id),
"user": vucem.usuario if vucem.usuario else "",
"password": vucem.password if vucem.password else "",
"efirma": vucem.efirma if vucem.efirma else "",
"key": key_path,
"cer": cer_path,
"is_active": vucem.is_active if vucem.is_active else False,
"organizacion": str(credenciales.organizacion.id) if credenciales.organizacion else ""
}
except CredencialesImportador.DoesNotExist:
return Response({"error": "No se encontró credencial VUCEM para la organización del pedimento"}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({"error": f"Error al buscar credencial VUCEM: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
try:
# Obtener la URL desde las variables de entorno
api_url = os.getenv('SERVICE_API_URL_V2')
logger.info(f"Usando MICROSERVICE_BASE_URL: {api_url}")
endpoint = f"{api_url.rstrip('/')}/services/auditar_pedimento_completo"
# endpoint = "http://localhost:8001/api/v2/services/auditar_pedimento_completo"
except requests.exceptions.RequestException as e:
logger.error(f"Error obteniendo MICROSERVICE_BASE_URL: {e}")
return Response({"error": "Error obteniendo la URL del microservicio", "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error inesperado obteniendo MICROSERVICE_BASE_URL: {e}")
return Response({"error": "Error inesperado obteniendo la URL del microservicio", "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
try:
resp = requests.post(endpoint, json=payload, timeout=30)
except requests.exceptions.RequestException as e:
logger.error(f"Error comunicándose con microservice: {e}")
return Response({"error": "No se pudo conectar con el microservicio", "detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
try:
content = resp.json()
except ValueError:
content = {"detail": resp.text}
return Response(content, status=resp.status_code)

View File

@@ -9,15 +9,10 @@ class ReportDocument(models.Model):
('ready', 'Listo'),
('error', 'Error'),
]
TYPE_REPORT = [
('cumplimiento', 'cumplimiento'),
('control_pedimento', 'control_pedimento'),
]
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='report_documents')
filters = models.JSONField(blank=True, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
file = models.FileField(upload_to='reports/', blank=True, null=True)
report_type = models.CharField(max_length=30, choices=TYPE_REPORT, default='cumplimiento')
error_message = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
finished_at = models.DateTimeField(blank=True, null=True)

View File

@@ -1,12 +1,9 @@
from celery import shared_task
from api.organization.models import Organizacion
from django.core.files.base import ContentFile
from django.utils import timezone
from api.reports.models import ReportDocument
from api.customs.models import Pedimento, Cove, EDocument, Partida
from django.db.models import Q, Exists, OuterRef
# from django.db.models import Q,
from api.record.models import Document
from django.db.models import Q
import csv
import os
from django.conf import settings
@@ -18,6 +15,7 @@ def generate_report_document(report_id):
report.status = 'processing'
report.save(update_fields=['status'])
filters = report.filters or {}
# Construir Q para filtros complejos
pedimentos_filters = Q()
if filters.get('organizacion_id'):
pedimentos_filters &= Q(organizacion_id=filters['organizacion_id'])
@@ -85,199 +83,3 @@ def generate_report_document(report_id):
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])
@shared_task
def generate_report_control_pedimento(report_id):
try:
report = ReportDocument.objects.get(id=report_id)
report.status = 'processing'
report.save(update_fields=['status'])
filters = report.filters or {}
# Construir filtros
pedimentos_filters = {}
if filters.get('organizacion_id'):
pedimentos_filters['organizacion_id'] = filters['organizacion_id']
if filters.get('fecha_pago__gte'):
pedimentos_filters['fecha_pago__gte'] = filters['fecha_pago__gte']
if filters.get('fecha_pago__lte'):
pedimentos_filters['fecha_pago__lte'] = filters['fecha_pago__lte']
if filters.get('pedimento_app'):
pedimentos_filters['pedimento_app'] = filters['pedimento_app']
# pedimentos por organizacion
pedimentos_qs = Pedimento.objects.filter(**pedimentos_filters)
pedimentos_total = pedimentos_qs.count()
pedimento_ids = list(pedimentos_qs.values_list('id', flat=True))
rfcs_raw = list(pedimentos_qs.values_list('agente_aduanal', flat=True))
# inicializar totales
pedimentos_completos = 0
total_documentos = 0
documentos_sin_descargar = 0
nombre_organizacion = ''
if filters.get('organizacion_id'):
try:
# Asumo que tienes un modelo Organizacion - ajusta según tu modelo real
organizacion = Organizacion.objects.get(id=filters['organizacion_id'])
nombre_organizacion = organizacion.nombre # ajusta el campo según tu modelo
except Organizacion.DoesNotExist:
nombre_organizacion = f"ID: {filters['organizacion_id']}"
except Exception as e:
nombre_organizacion = f"Error: {str(e)}"
# lista de rfc
rfc_list = ', '.join(sorted(set([rfc for rfc in rfcs_raw if rfc])))
fecha_inicio = ''
fecha_fin = ''
if pedimentos_qs.exists():
primer_pedimento = pedimentos_qs.order_by('fecha_pago').first()
if primer_pedimento and primer_pedimento.fecha_pago:
fecha_inicio = primer_pedimento.fecha_pago.strftime('%Y-%m-%d')
ultimo_pedimento = pedimentos_qs.order_by('-fecha_pago').first()
if ultimo_pedimento and ultimo_pedimento.fecha_pago:
fecha_fin = ultimo_pedimento.fecha_pago.strftime('%Y-%m-%d')
# Para cada pedimento, verificar si está completo
for pedimento in pedimentos_qs:
# Contar documentos de este pedimento
docs_pedimento = 0
docs_pendientes_pedimento = 0
# COVES
coves_count = Cove.objects.filter(pedimento_id=pedimento.id).count()
coves_pendientes = Cove.objects.filter(pedimento_id=pedimento.id, cove_descargado=False).count()
docs_pedimento += coves_count
docs_pendientes_pedimento += coves_pendientes
# PARTIDAS
partidas_count = Partida.objects.filter(pedimento_id=pedimento.id).count()
partidas_pendientes = Partida.objects.filter(pedimento_id=pedimento.id, descargado=False).count()
docs_pedimento += partidas_count
docs_pendientes_pedimento += partidas_pendientes
# EDOCUMENTS
edocs_count = EDocument.objects.filter(pedimento_id=pedimento.id).count()
edocs_pendientes = EDocument.objects.filter(pedimento_id=pedimento.id, edocument_descargado=False).count()
docs_pedimento += edocs_count
docs_pendientes_pedimento += edocs_pendientes
# Acumular totales
total_documentos += docs_pedimento
documentos_sin_descargar += docs_pendientes_pedimento
# Si no tiene documentos pendientes, está completo
if docs_pendientes_pedimento == 0 and docs_pedimento > 0:
pedimentos_completos += 1
# 3. PORCENTAJE
porcentaje_faltantes = (documentos_sin_descargar / total_documentos * 100) if total_documentos > 0 else 0
# 4. GENERAR CSV CON DETALLES
filename = f"report_{report.id}_{timezone.now().strftime('%Y%m%d%H%M%S')}.csv"
file_path = os.path.join(settings.MEDIA_ROOT, 'reports', filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
todas_las_filas = []
# Recopilar datos detallados - UNA FILA POR CADA DOCUMENTO
for pedimento in pedimentos_qs:
# DATOS BASE DEL PEDIMENTO (se repiten en cada fila)
datos_base_pedimento = [
pedimento.aduana or '',
pedimento.patente or '',
pedimento.regimen or '',
pedimento.pedimento or '', # No. Pedimento (7 dígitos)
pedimento.pedimento_app or '', # No. Pedimento App completo
pedimento.clave_pedimento or '',
pedimento.tipo_operacion.tipo if pedimento.tipo_operacion else '',
str(pedimento.contribuyente_id) if pedimento.contribuyente_id else ''
]
# COVES - Una fila por cada COVE
coves = Cove.objects.filter(pedimento_id=pedimento.id)
for cove in coves:
estado = 'VERDADERO' if cove.cove_descargado else 'FALSO'
fila = datos_base_pedimento + [
# str(cove.id), # Identificador de documento
cove.numero_cove,
'COVE', # Tipo de documento
estado
]
todas_las_filas.append(fila)
# PARTIDAS - Una fila por cada Partida
partidas = Partida.objects.filter(pedimento_id=pedimento.id)
for partida in partidas:
estado = 'VERDADERO' if partida.descargado else 'FALSO'
fila = datos_base_pedimento + [
# str(partida.id),
partida.numero_partida,
'PARTIDA', # Tipo de documento
estado
]
todas_las_filas.append(fila)
# EDOCUMENTS - Una fila por cada EDocument
edocuments = EDocument.objects.filter(pedimento_id=pedimento.id)
for edoc in edocuments:
estado = 'VERDADERO' if edoc.edocument_descargado else 'FALSO'
fila = datos_base_pedimento + [
# str(edoc.id),
edoc.numero_edocument,
'EDOCUMENT', # Tipo de documento
estado
]
todas_las_filas.append(fila)
# 5. ESCRIBIR ARCHIVO CSV
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# SECCIÓN DE TOTALES
writer.writerow(['RESUMEN DEL REPORTE - CONTROL DE PEDIMENTOS'])
writer.writerow(['ORGANIZACION:', nombre_organizacion])
writer.writerow([])
writer.writerow(['TOTAL DE EXPEDIENTES:', pedimentos_total])
writer.writerow(['TOTAL DE EXPEDIENTES COMPLETOS:', pedimentos_completos])
writer.writerow(['TOTAL DE DOCUMENTOS:', total_documentos])
writer.writerow(['DOCUMENTOS SIN DESCARGAR:', documentos_sin_descargar])
writer.writerow(['PORCENTAJE DE DOCUMENTOS FALTANTES (%):', f"{porcentaje_faltantes:.2f}%"])
writer.writerow(['DESDE: ', fecha_inicio, ' HASTA: ', fecha_fin])
writer.writerow(['LISTA RFC:', rfc_list])
writer.writerow([])
writer.writerow([])
# ENCABEZADOS DE DATOS (según requerimiento)
headers = [
'ADUANA', 'PATENTE', 'REGIMEN', 'NO. PEDIMENTO', 'PEDIMENTO_APP',
'CLAVE_PEDIMENTO', 'TIPO_OPERACION', 'CONTRIBUYENTE_ID',
'IDENTIFICADOR_DOCUMENTO', 'TIPO_DOCUMENTO', 'ESTADO'
]
writer.writerow(headers)
# DATOS DETALLADOS
for fila in todas_las_filas:
writer.writerow(fila)
with open(file_path, 'rb') as f:
report.file.save(filename, ContentFile(f.read()), save=True)
report.status = 'ready'
report.finished_at = timezone.now()
report.save(update_fields=['status', 'file', 'finished_at'])
except Exception as e:
report.status = 'error'
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])

View File

@@ -1,12 +1,10 @@
from django.urls import path, include
from .views import ExportModelView, ExportDataStageView, dashboard_summary
from .views import ExportModelView, dashboard_summary
# from .views_stats import documentos_por_fecha
from .views_table import table_summary, report_document_status, report_document_list, report_document_download, control_pedimento
from .views_table import table_summary, report_document_status, report_document_list, report_document_download
urlpatterns = [
path('exportmodel/', ExportModelView.as_view(), name='export-model'),
path('exportmodel/datastage/', ExportDataStageView.as_view(), name='export-datastage-model'),
path('control-pedimento/', control_pedimento, name='control_pedimento'),
path('dashboard/summary/', dashboard_summary, name='dashboard-summary'),
#path('documentos-por-fecha/', documentos_por_fecha, name='documentos-por-fecha'),
path('table-summary/', table_summary, name='table-summary'),

View File

@@ -48,10 +48,7 @@ from core.permissions import (
IsSuperUser
)
from .serializers import ExportModelSerializer
import uuid
import datetime
import zipfile
from django.db import models
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name)
@@ -89,797 +86,11 @@ def export_model_to_excel(request, model_name, fields, module='datastage', filte
response['Content-Disposition'] = f'attachment; filename="{model_name}.xlsx"'
return response
# class ControlPedimentoView(APIView):
# my_tags = ['Control-Pedimento']
# permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
# @swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
# def post(self, request, *args, **kwargs):
# """
# Endpoint específico para exportación de DataStage con soporte múltiple
# """
# # Verificar si es modo múltiple
# modo = request.data.get('modo', 'simple')
# if modo == 'multiple':
# return self.handle_multiple_export(request)
# else:
# return self.handle_simple_export(request)
class ExportDataStageView(APIView):
my_tags = ['Reportes-DataStage']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
# Constantes para partición
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
MAX_RECORDS_PER_FILE = 120000 # Límite seguro por archivo
def safe_excel_value(self, value):
"""
Convierte cualquier valor a un formato seguro para Excel
"""
if value is None:
return ''
elif isinstance(value, (uuid.UUID,)):
return str(value)
elif hasattr(value, 'uuid'):
return str(value.uuid)
elif hasattr(value, 'id'):
return str(value.id)
elif isinstance(value, (datetime.datetime, datetime.date)):
return value.isoformat()
elif isinstance(value, (dict, list)):
return str(value)
else:
return str(value)
@swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
def post(self, request, *args, **kwargs):
"""
Endpoint específico para exportación de DataStage con soporte múltiple
"""
# Verificar si es modo múltiple
modo = request.data.get('modo', 'simple')
if modo == 'multiple':
return self.handle_multiple_export(request)
else:
return self.handle_simple_export(request)
def handle_simple_export(self, request):
"""Maneja exportación simple de DataStage (un solo modelo)"""
model_name = request.data.get('model')
fields = request.data.get('fields')
global_filters = request.data.get('globalFilters', {})
export_type = request.data.get('format', 'csv')
module = 'datastage'
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
model = apps.get_model(module, model_name)
filters = self.apply_global_filters_to_model(global_filters, model, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if export_type == 'excel':
# Verificar si necesita partición
if total_records > self.MAX_RECORDS_PER_FILE:
return self.export_single_model_partitioned(request, model_name, fields, filters, total_records)
else:
return export_model_to_excel(request, model_name, fields, module, filters)
else:
if total_records > self.MAX_RECORDS_PER_FILE:
return self.export_single_model_csv_partitioned(request, model_name, fields, filters, total_records)
else:
return export_model_to_csv(request, model_name, fields, module, filters)
except LookupError:
return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND)
def handle_multiple_export(self, request):
"""Maneja exportación múltiple de DataStage (varios modelos)"""
models_data = request.data.get('models', [])
export_type = request.data.get('format', 'csv')
global_filters = request.data.get('globalFilters', {})
if not models_data:
return Response({'error': 'models are required for multiple export'}, status=status.HTTP_400_BAD_REQUEST)
related_keys = self.get_related_keys_from_filters(global_filters, models_data, request.user)
if export_type == 'excel':
# Siempre usar el método particionado inteligente para Excel
return self.export_datastage_multiple_partitioned_excel(request, models_data, global_filters, related_keys)
else:
# Para CSV, podemos mantener la lógica actual o mejorarla
total_estimated_records = self.estimate_total_records(models_data, global_filters, related_keys, request.user)
if total_estimated_records > self.MAX_RECORDS_PER_FILE:
return self.export_datastage_multiple_partitioned_csv(request, models_data, global_filters, related_keys)
else:
return self.export_datastage_multiple_to_csv(request, models_data, global_filters, related_keys)
def estimate_total_records(self, models_data, global_filters, related_keys, user):
"""Estima el total de registros para todos los modelos"""
total = 0
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, user)
total += model.objects.filter(**filters).count()
except:
continue
return total
def export_datastage_multiple_to_excel(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage con filtrado relacionado (múltiples hojas)"""
wb = openpyxl.Workbook()
wb.remove(wb.active)
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
# 🔥 APLICAR FILTROS RELACIONADOS
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
# Si hay filtros, aplicarlos; si no, obtener todos los registros
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none() # No obtener nada si no hay filtros
# Si no hay registros, saltar este modelo
if queryset.count() == 0:
continue
# Crear hoja (limitar nombre a 31 caracteres)
sheet_name = model_name[:31]
ws = wb.create_sheet(title=sheet_name)
# Escribir encabezados
ws.append(fields)
# Escribir datos
for row in queryset:
row_values = []
for field in fields:
value = row[field]
# 🔥 USAR safe_excel_value para convertir valores
row_values.append(self.safe_excel_value(value))
ws.append(row_values)
except LookupError:
continue
# Si no se crearon hojas, crear una vacía
if len(wb.sheetnames) == 0:
ws = wb.create_sheet(title="Sin datos")
ws.append(["No se encontraron datos para los modelos especificados"])
output = io.BytesIO()
wb.save(output)
output.seek(0)
response = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = 'attachment; filename="datastage_related_report.xlsx"'
return response
def export_datastage_multiple_partitioned_excel(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos Excel particionados inteligentemente"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
file_counter = 1
current_wb = None
current_file_records_count = 0
MAX_SHEETS_PER_FILE = 10 # Límite de hojas por archivo Excel
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Si el modelo necesita particionarse (más de MAX_RECORDS_PER_FILE)
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Verificar si necesitamos crear nuevo archivo
# 1. Si no hay archivo actual
# 2. Si ya tenemos muchas hojas en este archivo
# 3. Si este archivo ya está "lleno" (muchos registros)
if (current_wb is None or
len(current_wb.sheetnames) >= MAX_SHEETS_PER_FILE or
current_file_records_count > self.MAX_RECORDS_PER_FILE * 3): # ~150K registros
if current_wb is not None:
# Guardar archivo actual en ZIP
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
file_counter += 1
# Crear nuevo workbook
current_wb = openpyxl.Workbook()
current_wb.remove(current_wb.active) # Remover hoja por defecto
current_file_records_count = 0
# Crear hoja para esta parte del modelo
sheet_name = f"{model_name[:25]}_p{page_num}"
ws = current_wb.create_sheet(title=sheet_name[:31])
ws.append(fields)
# Escribir datos
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
current_file_records_count += len(page.object_list)
else:
# Modelo pequeño (≤ MAX_RECORDS_PER_FILE)
# Verificar si necesitamos nuevo archivo
if (current_wb is None or
len(current_wb.sheetnames) >= MAX_SHEETS_PER_FILE or
current_file_records_count + total_records > self.MAX_RECORDS_PER_FILE * 3):
if current_wb is not None:
# Guardar archivo actual
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
file_counter += 1
# Crear nuevo workbook
current_wb = openpyxl.Workbook()
current_wb.remove(current_wb.active)
current_file_records_count = 0
# Crear hoja para este modelo
sheet_name = model_name[:31]
ws = current_wb.create_sheet(title=sheet_name)
ws.append(fields)
# Escribir datos
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
current_file_records_count += total_records
except LookupError:
continue
# Guardar el último workbook si existe
if current_wb is not None:
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
except Exception as e:
return Response({'error': f'Error en exportación particionada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_datastage_multiple_to_csv(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos CSV en ZIP"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if total_records == 0:
continue
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
except LookupError:
continue
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
def export_datastage_multiple_partitioned_csv(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos CSV particionados en ZIP"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if total_records == 0:
continue
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}_part{page_num}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
else:
# Modelo pequeño, exportar completo
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
# Escribir encabezados
writer.writerow(fields)
# Escribir datos
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
except LookupError as e:
continue
except Exception as e:
continue
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
except Exception as e:
return Response({'error': f'Error en exportación CSV particionada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_single_model_partitioned(self, request, model_name, fields, filters, total_records):
"""Exporta un solo modelo particionado a ZIP"""
try:
zip_buffer = io.BytesIO()
module = 'datastage'
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**filters).values(*fields)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear Excel para esta parte
wb = openpyxl.Workbook()
ws = wb.active
ws.title = f"Parte_{page_num}"[:31]
ws.append(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}_part{page_num}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename="{model_name}_particionado.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error exportando modelo: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_single_model_csv_partitioned(self, request, model_name, fields, filters, total_records):
"""Exporta un solo modelo CSV particionado a ZIP"""
try:
zip_buffer = io.BytesIO()
module = 'datastage'
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**filters).values(*fields)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}_part{page_num}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename="{model_name}_particionado.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error exportando modelo CSV: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_related_keys_from_filters(self, global_filters, models_data, user):
"""
Obtiene patentes, pedimentos y datastages que cumplen EXACTAMENTE con TODOS los filtros globales
VERSIÓN SIMPLIFICADA - Usa la MISMA lógica que apply_global_filters_to_model
"""
related_keys = {
'patentes': set(),
'pedimentos': set(),
'datastage_ids': set()
}
# Si no hay filtros, retornar vacío
if not any(v for v in global_filters.values() if v not in [None, '']):
return {}
all_records_with_filters = []
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
# ¡USAR LA MISMA FUNCIÓN QUE EN MODO SINGULAR!
filters = self.apply_global_filters_to_model(global_filters, model, user)
if filters:
# EJECUTAR CONSULTA - IDÉNTICO A MODO SINGULAR
queryset = model.objects.filter(**filters)
total = queryset.count()
# VERIFICACIÓN ESPECIAL PARA RFC
if 'rfc' in filters:
rfc_value = filters['rfc']
# Doble verificación: contar registros con ese RFC exacto
rfc_exact_count = queryset.filter(rfc=rfc_value).count()
if rfc_exact_count != total:
try:
other_rfcs = queryset.exclude(rfc=rfc_value).values_list('rfc', flat=True).distinct()[:5]
except:
pass
# Obtener registros
records = queryset.values('patente', 'pedimento', 'datastage_id')
all_records_with_filters.extend(list(records))
except LookupError:
continue
if not all_records_with_filters:
return {'patentes': set(), 'pedimentos': set(), 'datastage_ids': set()}
for record in all_records_with_filters:
if record.get('patente'):
related_keys['patentes'].add(record['patente'])
if record.get('pedimento'):
related_keys['pedimentos'].add(record['pedimento'])
if record.get('datastage_id'):
related_keys['datastage_ids'].add(record['datastage_id'])
return {k: list(v) for k, v in related_keys.items() if v}
def apply_global_filters_to_model(self, global_filters, model, user):
"""
Aplica filtros globales - VERSIÓN CORREGIDA CON UUID
"""
filters = {}
model_fields = [f.name for f in model._meta.get_fields()]
# ORGANIZACIÓN - Manejar como UUID
org_value = global_filters.get('organizacion')
if org_value and org_value != '' and 'organizacion' in model_fields:
field = model._meta.get_field('organizacion')
if hasattr(field, 'related_model'): # Es ForeignKey
# Convertir string a UUID
try:
import uuid
org_uuid = uuid.UUID(org_value)
filters['organizacion_id'] = org_uuid
except Exception as e:
# Fallback: dejar como string (puede no funcionar)
filters['organizacion_id'] = org_value
else: # Es CharField
filters['organizacion'] = org_value
# RFC - Manejar normalmente
rfc_value = global_filters.get('rfc')
if rfc_value and rfc_value != '' and 'rfc' in model_fields:
filters['rfc'] = rfc_value
# PATENTE
if global_filters.get('patente'):
filters['patente'] = global_filters['patente']
# PEDIMENTO
if global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
# FECHAS
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
return filters
def apply_related_filters(self, global_filters, model, related_keys, user):
filters = {}
model_fields = [f.name for f in model._meta.get_fields()]
# 1. Organización
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
# 2. RFC (¡ESTO ES LO QUE FALTA!)
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
# 3. Fechas (SIEMPRE se aplican)
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
# 🔥 SEGUNDO: Si hay related_keys, AÑADIRLAS a los filtros existentes
if any(related_keys.values()):
# Añadir patentes si existen
if related_keys.get('patentes') and 'patente' in model_fields:
filters['patente__in'] = related_keys['patentes']
# Añadir pedimentos si existen
if related_keys.get('pedimentos') and 'pedimento' in model_fields:
filters['pedimento__in'] = related_keys['pedimentos']
# Añadir datastage_ids si existen
if related_keys.get('datastage_ids') and 'datastage_id' in model_fields:
filters['datastage_id__in'] = related_keys['datastage_ids']
else:
# Solo patente y pedimento específicos (no listas)
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
return filters
def estimate_excel_file_size(self, num_records, num_columns):
"""Estima tamaño aproximado del archivo Excel"""
# Estimación aproximada: 100 bytes por celda
return num_records * num_columns * 100
def export_with_size_control(self, request, models_data, global_filters, related_keys):
"""Versión con control de tamaño de archivo"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
file_counter = 1
current_wb = None
current_file_size_estimate = 0
MAX_FILE_SIZE_ESTIMATE = 50 * 1024 * 1024 # 50MB estimado
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Calcular tamaño estimado para este modelo
model_size_estimate = self.estimate_excel_file_size(total_records, len(fields))
# Si el modelo es muy grande o no cabe en el archivo actual
needs_new_file = (
current_wb is None or
current_file_size_estimate + model_size_estimate > MAX_FILE_SIZE_ESTIMATE or
(total_records > self.MAX_RECORDS_PER_FILE and current_file_size_estimate > 0)
)
if needs_new_file and current_wb is not None:
# Guardar archivo actual
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
file_counter += 1
current_wb = None
current_file_size_estimate = 0
if current_wb is None:
current_wb = openpyxl.Workbook()
current_wb.remove(current_wb.active)
# Manejar modelos que exceden el límite por hoja
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear hoja para esta parte
sheet_name = f"{model_name[:20]}_p{page_num}"[:31]
ws = current_wb.create_sheet(title=sheet_name)
ws.append(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
# Actualizar tamaño estimado
page_size = self.estimate_excel_file_size(len(page.object_list), len(fields))
current_file_size_estimate += page_size
else:
# Modelo pequeño, una hoja
sheet_name = model_name[:31]
ws = current_wb.create_sheet(title=sheet_name)
ws.append(fields)
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
current_file_size_estimate += model_size_estimate
except LookupError:
continue
# Guardar último archivo si existe
if current_wb is not None:
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
except Exception as e:
return Response({'error': f'Error: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ExportModelView(APIView):
my_tags = ['Reportes']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated & (
IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
@swagger_auto_schema(
manual_parameters=[

View File

@@ -1,5 +1,5 @@
from api.reports.models import ReportDocument
from api.reports.tasks.report_document import generate_report_document, generate_report_control_pedimento
from api.reports.tasks.report_document import generate_report_document
from django.http import FileResponse
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
@@ -11,10 +11,7 @@ def table_summary(request):
"""
Solo dispara la tarea asíncrona para generar el reporte CSV. No consulta ni procesa datos.
"""
org_id = request.query_params.get('organizacion_id')
# hasta aqui si llega y crea el registro en la base de datos
print(f'🖼️🖼️🖼️🖼️🖼️🖼️🖼️ table_summary organizacion id = {org_id}')
if not org_id:
return Response({"error": "organizacion_id es requerido"}, status=400)
# Obtener filtros de query params
@@ -63,8 +60,7 @@ def table_summary(request):
report = ReportDocument.objects.create(
user=request.user,
filters=filtros,
status='pending',
report_type='cumplimiento'
status='pending'
)
generate_report_document.delay(report.id)
return Response({
@@ -98,7 +94,6 @@ def report_document_list(request):
data = [
{
"report_id": r.id,
"report_type": r.report_type,
"status": r.status,
"created_at": r.created_at,
"finished_at": r.finished_at,
@@ -119,50 +114,4 @@ def report_document_download(request, report_id):
response = FileResponse(report.file.open('rb'), as_attachment=True, filename=report.file.name)
return response
except ReportDocument.DoesNotExist:
return Response({"error": "Reporte no encontrado"}, status=404)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def control_pedimento(request):
"""
Dispara la tarea asíncrona para generar el reporte CSV de control de Pedimentos.
"""
org_id = request.query_params.get('organizacion_id')
if not org_id:
return Response({"error": "organizacion_id es requerido"}, status=400)
# Simplificar la lógica de fechas
fecha_pago_gte = request.query_params.get('fecha_pago__gte')
fecha_pago_lte = request.query_params.get('fecha_pago__lte')
pedimento_app = request.query_params.get('pedimento_app')
# Si las fechas vienen como string, mantenerlas como están
fecha_pago_gte_str = fecha_pago_gte if fecha_pago_gte else None
fecha_pago_lte_str = fecha_pago_lte if fecha_pago_lte else None
filtros = {
"pedimento_app": pedimento_app,
"organizacion_id": org_id,
"fecha_pago__gte": fecha_pago_gte_str,
"fecha_pago__lte": fecha_pago_lte_str,
}
# Crear el reporte
report = ReportDocument.objects.create(
user=request.user,
filters=filtros,
status='pending',
report_type='control_pedimento'
)
# Disparar la tarea asíncrona
generate_report_control_pedimento.delay(report.id)
return Response({
"report_id": report.id,
"status": report.status,
"created_at": report.created_at,
"message": "Reporte en proceso de generación",
"download_url": report.file.url if report.file else None
}, status=202)
return Response({"error": "Reporte no encontrado"}, status=404)