fix/de los tickets T2026-05-027, T2025-09-004 y T2025-09-056

This commit is contained in:
2026-06-15 11:18:58 -06:00
parent 7644446267
commit 23ed52c78a
29 changed files with 2992 additions and 987 deletions

View File

@@ -25,7 +25,10 @@ from core.permissions import (
require_permission,
user_has_permission,
)
from .models import ReportDocument
from .serializers import ExportModelSerializer
from .services import datastage_export
from .tasks.report_datastage import generate_report_datastage
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name)
@@ -90,28 +93,13 @@ class ExportDataStageView(APIView):
return [IsAuthenticated(), require_permission('reportes.view')()]
return [IsAuthenticated(), require_permission('reportes.export')()]
# Constantes para partición
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
MAX_RECORDS_PER_FILE = 120000 # Límite seguro por archivo
# La lógica de exportación vive en services/datastage_export.py (la usa la
# task Celery generate_report_datastage); estos delegados conservan la
# interfaz para los métodos legacy de esta clase.
MAX_RECORDS_PER_FILE = datastage_export.MAX_RECORDS_PER_FILE
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)
return datastage_export.safe_excel_value(value)
def get(self, request, *args, **kwargs):
"""Retorna RFCs distintos de Registro501 para la organización activa del usuario."""
@@ -134,19 +122,67 @@ class ExportDataStageView(APIView):
except LookupError:
return Response({'rfcs': []})
@swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
@swagger_auto_schema(request_body=ExportModelSerializer, responses={202: 'Reporte encolado (Celery)'})
def post(self, request, *args, **kwargs):
"""
Endpoint específico para exportación de DataStage con soporte múltiple
Encola la generación asíncrona del reporte DataStage (Celery + SSE).
Responde 202 con report_id y task_id; el progreso se sigue por SSE
(/stream/tasks/{task_id}) y el archivo se descarga después vía
/reports/report-document-download/{report_id}/.
"""
# Verificar si es modo múltiple
modo = request.data.get('modo', 'simple')
export_format = request.data.get('format', 'csv')
global_filters = request.data.get('globalFilters', {})
# Validar payload antes de encolar (mismos errores que el flujo síncrono)
if modo == 'multiple':
return self.handle_multiple_export(request)
models_data = request.data.get('models', [])
if not models_data:
return Response({'error': 'models are required for multiple export'}, status=status.HTTP_400_BAD_REQUEST)
else:
return self.handle_simple_export(request)
model_name = request.data.get('model')
fields = request.data.get('fields')
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
apps.get_model('datastage', model_name)
except LookupError:
return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND)
global_filters, err = self._resolve_org_filter(global_filters, request.user)
if err:
return err
# La org ya resuelta viaja en el payload: la task no tiene request.user
payload = {
'modo': modo,
'format': export_format,
'globalFilters': global_filters,
'organizacion_id': global_filters.get('organizacion'),
}
if modo == 'multiple':
payload['models'] = models_data
else:
payload['model'] = model_name
payload['fields'] = fields
report = ReportDocument.objects.create(
user=request.user,
filters=payload,
status='pending',
report_type='datastage',
)
task = generate_report_datastage.delay(report.id)
return Response({
'report_id': report.id,
'task_id': task.id,
'status': report.status,
'created_at': report.created_at,
'download_url': None,
}, status=status.HTTP_202_ACCEPTED)
def _resolve_org_filter(self, global_filters, user):
"""
Devuelve los global_filters asegurando que siempre haya una organización.
@@ -164,63 +200,6 @@ class ExportDataStageView(APIView):
filters['organizacion'] = str(org.id)
return filters, None
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)
global_filters, err = self._resolve_org_filter(global_filters, request.user)
if err:
return err
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)
global_filters, err = self._resolve_org_filter(global_filters, request.user)
if err:
return err
related_keys = self.get_related_keys_from_filters(global_filters, models_data, request.user)
if export_type == 'excel':
return self.export_datastage_multiple_partitioned_excel_agrupados(request, models_data, global_filters, related_keys)
else:
return self.export_datastage_multiple_to_csv_combined(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
@@ -297,235 +276,6 @@ class ExportDataStageView(APIView):
response['Content-Disposition'] = 'attachment; filename="datastage_related_report.xlsx"'
return response
def export_datastage_multiple_partitioned_excel_agrupados(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage agrupados en la misma hoja de Excel, con particionado por límite de registros"""
try:
from api.organization.models import Organizacion
org_mapping = {str(org.id): org.nombre for org in Organizacion.objects.all()}
# 1. Recopilar todos los datos FUERA del contexto ZIP
all_models_data = {}
model_field_mappings = {}
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
normalized_fields = []
for f in fields:
try:
key = f.strip() if isinstance(f, str) else f
except Exception:
key = f
if isinstance(key, str) and key.lower() == 'organizacion':
if 'organizacion_id' not in normalized_fields:
normalized_fields.append('organizacion_id')
else:
if key not in normalized_fields:
normalized_fields.append(key)
fields = normalized_fields
required_fields = ['seccion_aduanera', 'patente', 'pedimento']
for field in required_fields:
if field not in fields:
fields.append(field)
if 'organizacion_id' not in fields and 'organizacion_id' in [f.name for f in apps.get_model('datastage', model_name)._meta.get_fields()]:
fields.append('organizacion_id')
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()
if queryset.count() == 0:
continue
relation_fields = [fn for fn in ['seccion_aduanera', 'patente', 'pedimento'] if fn in fields]
if not relation_fields:
relation_fields = ['datastage_id'] if 'datastage_id' in fields else [fields[0]]
if model_name not in model_field_mappings:
model_field_mappings[model_name] = fields
for record in queryset:
key_parts = [str(record[rf]) for rf in relation_fields if rf in record and record[rf] is not None]
if not key_parts:
import hashlib
key = hashlib.md5(str(sorted(record.items())).encode()).hexdigest()[:10]
else:
key = "_".join(key_parts)
processed_record = {}
for field_name, value in record.items():
if field_name == 'organizacion_id' and value:
org_id_str = str(value)
if org_id_str in org_mapping:
processed_value = org_mapping[org_id_str]
else:
try:
org = Organizacion.objects.filter(id=value).first()
processed_value = org.nombre if org else org_id_str
org_mapping[org_id_str] = processed_value
except Exception:
processed_value = org_id_str
else:
processed_value = value
if field_name in relation_fields:
prefixed_field_name = field_name
else:
prefixed_field_name = f"{model_name}_{field_name}"
if field_name == 'organizacion_id':
prefixed_field_name = prefixed_field_name.replace('organizacion_id', 'organizacion_nombre')
processed_record[prefixed_field_name] = self.safe_excel_value(processed_value)
if key not in all_models_data:
all_models_data[key] = {'relation_fields': {}, 'model_records': {}}
for rel_field in relation_fields:
if rel_field in record:
all_models_data[key]['relation_fields'][rel_field] = record[rel_field]
if model_name not in all_models_data[key]['model_records']:
all_models_data[key]['model_records'][model_name] = []
all_models_data[key]['model_records'][model_name].append(processed_record)
except LookupError:
continue
# 2. Sin datos → Excel vacío (no JSON 404 que rompe la descarga en el frontend)
if not all_models_data:
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sin datos"
ws.append(["No se encontraron datos para los filtros especificados"])
output = io.BytesIO()
wb.save(output)
output.seek(0)
resp = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
resp['Content-Disposition'] = 'attachment; filename="datastage_sin_datos.xlsx"'
return resp
# 3. Construir filas combinadas — repetir el último registro en lugar de dejar vacíos
combined_rows = []
for key, data in all_models_data.items():
relation_fields_data = data['relation_fields']
model_records = data['model_records']
max_records_per_key = max((len(recs) for recs in model_records.values()), default=1)
for i in range(max_records_per_key):
row_data = {}
for rel_field, rel_value in relation_fields_data.items():
row_data[rel_field] = self.safe_excel_value(rel_value)
for model_name, records in model_records.items():
# Usar posición i o el último registro disponible
record = records[i] if i < len(records) else records[-1]
for field_name, value in record.items():
row_data[field_name] = value
combined_rows.append(row_data)
# 4. Encabezados ordenados
all_fields_set = set()
for row in combined_rows:
all_fields_set.update(row.keys())
all_fields = []
for rel_field in ['seccion_aduanera', 'patente', 'pedimento']:
if rel_field in all_fields_set:
all_fields.append(rel_field)
all_fields_set.discard(rel_field)
org_fields = sorted(f for f in all_fields_set if 'organizacion' in f.lower())
for org_field in org_fields:
all_fields.append(org_field)
all_fields_set.discard(org_field)
all_fields.extend(sorted(all_fields_set))
# 5. Filas de título y fecha de generación
now_str = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')
title_row = ["Reporte Datastage"]
date_row = [f"Generado: {now_str}"]
def _write_sheet(ws, sheet_name, page_rows):
ws.title = sheet_name[:31]
ws.append(title_row)
ws.append(date_row)
ws.append([])
ws.append(all_fields)
for row_data in page_rows:
ws.append([row_data.get(field, '') for field in all_fields])
for column in ws.columns:
max_length = 0
col_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except Exception:
pass
ws.column_dimensions[col_letter].width = min(max_length + 2, 50)
# 6. Excel directo si cabe en un archivo; ZIP solo si se necesita particionar
from django.core.paginator import Paginator
paginator = Paginator(combined_rows, self.MAX_RECORDS_PER_FILE)
if paginator.num_pages == 1:
wb = openpyxl.Workbook()
_write_sheet(wb.active, "Datastage", paginator.page(1).object_list)
output = io.BytesIO()
wb.save(output)
output.seek(0)
resp = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
resp['Content-Disposition'] = 'attachment; filename="datastage_reporte.xlsx"'
return resp
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for page_num in paginator.page_range:
page = paginator.page(page_num)
current_wb = openpyxl.Workbook()
_write_sheet(current_wb.active, f"Datastage_p{page_num}", page.object_list)
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{page_num}.xlsx", part_buffer.getvalue())
zip_buffer.seek(0)
resp = HttpResponse(zip_buffer.read(), content_type='application/zip')
resp['Content-Disposition'] = 'attachment; filename="datastage_combinado.zip"'
return resp
except Exception as e:
import traceback
import logging
logging.getLogger(__name__).error("Error en exportación combinada: %s", traceback.format_exc())
return Response({'error': f'Error en exportación combinada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_datastage_multiple_partitioned_excel_test_3(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage agrupados en la misma hoja de Excel, con particionado por límite de registros"""
try:
@@ -1215,144 +965,6 @@ class ExportDataStageView(APIView):
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_combined(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos combinados en un único CSV plano (misma lógica de agrupación que el Excel)."""
import hashlib
import logging
import traceback
logger = logging.getLogger(__name__)
try:
from api.organization.models import Organizacion
org_mapping = {str(org.id): org.nombre for org in Organizacion.objects.all()}
all_models_data = {}
model_field_mappings = {}
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
normalized_fields = []
for f in fields:
key = f.strip() if isinstance(f, str) else f
if isinstance(key, str) and key.lower() == 'organizacion':
if 'organizacion_id' not in normalized_fields:
normalized_fields.append('organizacion_id')
else:
if key not in normalized_fields:
normalized_fields.append(key)
fields = normalized_fields
for req_field in ['seccion_aduanera', 'patente', 'pedimento']:
if req_field not in fields:
fields.append(req_field)
try:
model = apps.get_model('datastage', model_name)
model_field_names = [f.name for f in model._meta.get_fields() if hasattr(f, 'name')]
if 'organizacion_id' not in fields and 'organizacion_id' in model_field_names:
fields.append('organizacion_id')
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields) if filters else model.objects.none()
if queryset.count() == 0:
continue
relation_fields = [fn for fn in ['seccion_aduanera', 'patente', 'pedimento'] if fn in fields]
if not relation_fields:
relation_fields = ['datastage_id'] if 'datastage_id' in fields else [fields[0]]
if model_name not in model_field_mappings:
model_field_mappings[model_name] = fields
for record in queryset:
key_parts = [str(record[rf]) for rf in relation_fields if rf in record and record[rf] is not None]
key = "_".join(key_parts) if key_parts else hashlib.md5(str(sorted(record.items())).encode()).hexdigest()[:10]
processed_record = {}
for field_name, value in record.items():
if field_name == 'organizacion_id' and value:
org_id_str = str(value)
processed_value = org_mapping.get(org_id_str, org_id_str)
else:
processed_value = value
if field_name in relation_fields:
prefixed = field_name
else:
prefixed = f"{model_name}_{field_name}"
if field_name == 'organizacion_id':
prefixed = prefixed.replace('organizacion_id', 'organizacion_nombre')
processed_record[prefixed] = self.safe_excel_value(processed_value)
if key not in all_models_data:
all_models_data[key] = {'relation_fields': {}, 'model_records': {}}
for rel_field in relation_fields:
if rel_field in record:
all_models_data[key]['relation_fields'][rel_field] = record[rel_field]
if model_name not in all_models_data[key]['model_records']:
all_models_data[key]['model_records'][model_name] = []
all_models_data[key]['model_records'][model_name].append(processed_record)
except LookupError:
continue
# Sin datos → CSV con mensaje, no error HTTP
if not all_models_data:
buf = io.StringIO()
csv.writer(buf).writerow(['No se encontraron datos para los filtros especificados'])
resp = HttpResponse(buf.getvalue(), content_type='text/csv; charset=utf-8')
resp['Content-Disposition'] = 'attachment; filename="datastage_sin_datos.csv"'
return resp
# Construir filas planas
combined_rows = []
for key, data in all_models_data.items():
relation_fields_data = data['relation_fields']
model_records = data['model_records']
max_records = max((len(recs) for recs in model_records.values()), default=1)
for i in range(max_records):
row_data = {}
for rel_field, rel_value in relation_fields_data.items():
row_data[rel_field] = self.safe_excel_value(rel_value)
for mn, records in model_records.items():
record = records[i] if i < len(records) else records[-1]
for field_name, value in record.items():
row_data[field_name] = value
combined_rows.append(row_data)
# Encabezados: campos de relación primero, luego org, luego el resto
all_fields_set = set()
for row in combined_rows:
all_fields_set.update(row.keys())
all_fields = []
for rel_field in ['seccion_aduanera', 'patente', 'pedimento']:
if rel_field in all_fields_set:
all_fields.append(rel_field)
all_fields_set.discard(rel_field)
org_fields = sorted(f for f in all_fields_set if 'organizacion' in f.lower())
for org_field in org_fields:
all_fields.append(org_field)
all_fields_set.discard(org_field)
all_fields.extend(sorted(all_fields_set))
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(all_fields)
for row_data in combined_rows:
writer.writerow([row_data.get(field, '') for field in all_fields])
resp = HttpResponse(buf.getvalue(), content_type='text/csv; charset=utf-8')
resp['Content-Disposition'] = 'attachment; filename="datastage_reporte.csv"'
return resp
except Exception as e:
logger.error("Error en exportación CSV combinada: %s", traceback.format_exc())
return Response({'error': f'Error en exportación CSV combinada: {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()
@@ -1472,254 +1084,14 @@ class ExportDataStageView(APIView):
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):
"""
Construye el conjunto de (patente, pedimento, datastage_id) que servirá como
llave de cruce entre modelos.
return datastage_export.get_related_keys_from_filters(global_filters, models_data)
Regla clave: si el filtro RFC está activo, solo los modelos que tienen el campo
'rfc' pueden contribuir a related_keys. Los modelos sin 'rfc' (ej. 505, 506)
no se usan como semilla — solo se filtrarán más tarde usando las claves ya
construidas, evitando que contaminen el resultado con pedimentos de otros RFC.
"""
related_keys = {
'patentes': set(),
'pedimentos': set(),
'datastage_ids': set()
}
# Sin filtros significativos → sin cruce
if not any(v for v in global_filters.values() if v not in [None, '']):
return {}
rfc_filter_active = bool(global_filters.get('rfc'))
date_filter_active = bool(global_filters.get('fecha_pago_desde') or global_filters.get('fecha_pago_hasta'))
all_records_with_filters = []
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
model_field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
# Un modelo puede ser semilla de related_keys SOLO si tiene campos
# para aplicar TODOS los filtros activos. Un modelo sin 'rfc' no puede
# ser semilla cuando hay filtro de RFC (contaminaría con pedimentos de
# otros RFCs). Igual para fecha_pago_real cuando hay filtro de fechas.
if rfc_filter_active and 'rfc' not in model_field_names:
continue
if date_filter_active and 'fecha_pago_real' not in model_field_names:
continue
filters = self.apply_global_filters_to_model(global_filters, model, user)
if not filters:
continue
records = model.objects.filter(**filters).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
return datastage_export.apply_global_filters_to_model(global_filters, model)
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 — convertir a UUID igual que apply_global_filters_to_model
if 'organizacion' in model_fields and global_filters.get('organizacion'):
org_value = global_filters['organizacion']
try:
field = model._meta.get_field('organizacion')
if hasattr(field, 'related_model'):
filters['organizacion_id'] = uuid.UUID(org_value)
else:
filters['organizacion'] = org_value
except Exception:
filters['organizacion_id'] = org_value
# 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
return datastage_export.apply_related_filters(global_filters, model, related_keys)
def estimate_excel_file_size(self, num_records, num_columns):
"""Estima tamaño aproximado del archivo Excel"""