diff --git a/api/reports/migrations/0001_initial.py b/api/reports/migrations/0001_initial.py new file mode 100644 index 0000000..c2f3092 --- /dev/null +++ b/api/reports/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.3 on 2025-10-21 23:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ReportDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('filters', models.JSONField(blank=True, null=True)), + ('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'Procesando'), ('ready', 'Listo'), ('error', 'Error')], default='pending', max_length=20)), + ('file', models.FileField(blank=True, null=True, upload_to='reports/')), + ('error_message', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('finished_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='report_documents', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/api/reports/migrations/__init__.py b/api/reports/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/reports/models.py b/api/reports/models.py index 71a8362..b68985b 100644 --- a/api/reports/models.py +++ b/api/reports/models.py @@ -1,3 +1,21 @@ -from django.db import models -# Create your models here. +from django.db import models +from django.contrib.auth import get_user_model + +class ReportDocument(models.Model): + STATUS_CHOICES = [ + ('pending', 'Pendiente'), + ('processing', 'Procesando'), + ('ready', 'Listo'), + ('error', 'Error'), + ] + 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) + error_message = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + finished_at = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return f"Reporte {self.id} - {self.status}" diff --git a/api/reports/tasks/report_document.py b/api/reports/tasks/report_document.py new file mode 100644 index 0000000..9a99ee5 --- /dev/null +++ b/api/reports/tasks/report_document.py @@ -0,0 +1,85 @@ +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 + +@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 {} + # Construir Q para filtros complejos + 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']) + # Consulta asíncrona de los modelos + 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, '' + ]) + # Guardar el archivo en el modelo + 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']) diff --git a/api/reports/urls.py b/api/reports/urls.py index 6318f8d..d05deff 100644 --- a/api/reports/urls.py +++ b/api/reports/urls.py @@ -1,12 +1,14 @@ from django.urls import path, include from .views import ExportModelView, dashboard_summary # from .views_stats import documentos_por_fecha -from .views_table import table_summary +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('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'), - + path('report-document-status//', report_document_status, name='report_document_status'), + path('report-document-list/', report_document_list, name='report_document_list'), + path('report-document-download//', report_document_download, name='report_document_download'), ] \ No newline at end of file diff --git a/api/reports/views_table.py b/api/reports/views_table.py index 23476c1..53f4528 100644 --- a/api/reports/views_table.py +++ b/api/reports/views_table.py @@ -1,34 +1,19 @@ +from api.reports.models import ReportDocument +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 from rest_framework.response import Response -from rest_framework.pagination import PageNumberPagination -from django.db.models import Value, CharField, Q, Exists, OuterRef, Subquery -from django.db.models.functions import Cast -from datetime import datetime, timedelta -from api.customs.models import Pedimento, Cove, EDocument, Partida - -class CustomPagination(PageNumberPagination): - page_size = 50 - page_size_query_param = 'page_size' - max_page_size = 1000 @api_view(['GET']) @permission_classes([IsAuthenticated]) def table_summary(request): - # ...existing code... - - # Si se solicita CSV, generar archivo y devolverlo (después de definir pedimentos_filters) - - - my_tags = ['Reportes'] """ - Endpoint que devuelve un resumen tabulado de pedimentos y sus documentos asociados. + Solo dispara la tarea asíncrona para generar el reporte CSV. No consulta ni procesa datos. """ - org_id = request.query_params.get('organizacion_id') if not org_id: return Response({"error": "organizacion_id es requerido"}, status=400) - # Obtener filtros de query params tipo_documento = request.query_params.get('tipo_documento') rfc = request.query_params.get('contribuyente__rfc') @@ -40,172 +25,86 @@ def table_summary(request): pedimento_app = request.query_params.get('pedimento_app') regimen = request.query_params.get('regimen') tipo_operacion = request.query_params.get('tipo_operacion') - # Si no se proporcionan fechas, establecer un rango por defecto de los últimos 30 días if not fecha_pago_gte and not fecha_pago_lte: + from datetime import datetime, timedelta fecha_pago_lte = datetime.now().date() fecha_pago_gte = fecha_pago_lte - timedelta(days=30) - - # Construir filtros base para pedimentos - pedimentos_filters = Q() - pedimentos_filters &= Q(organizacion_id=org_id) - if fecha_pago_gte: - pedimentos_filters &= Q(fecha_pago__gte=fecha_pago_gte) - if fecha_pago_lte: - pedimentos_filters &= Q(fecha_pago__lte=fecha_pago_lte) - if rfc: - pedimentos_filters &= Q(contribuyente__rfc=rfc) - if patente: - pedimentos_filters &= Q(patente=patente) - if aduana: - pedimentos_filters &= Q(aduana=aduana) - if pedimento: - pedimentos_filters &= Q(pedimento=pedimento) - if pedimento_app: - pedimentos_filters &= Q(pedimento_app=pedimento_app) - if regimen: - pedimentos_filters &= Q(regimen=regimen) - if tipo_operacion: - pedimentos_filters &= Q(tipo_operacion_id=tipo_operacion) - - # Si se solicita los últimos 100 registros actualizados - if request.query_params.get('ultimos') == '1': - pedimentos = Pedimento.objects.filter(pedimentos_filters).order_by('-updated_at')[:100] + # Crear el registro y lanzar la tarea Celery + filename_param = request.query_params.get('filename') + if filename_param: + filename = filename_param else: - pedimentos = Pedimento.objects.filter(pedimentos_filters) + filename = None + filtros = { + "organizacion_id": org_id, + "tipo_documento": tipo_documento, + "contribuyente__rfc": rfc, + "fecha_pago__gte": fecha_pago_gte, + "fecha_pago__lte": fecha_pago_lte, + "patente": patente, + "aduana": aduana, + "pedimento": pedimento, + "pedimento_app": pedimento_app, + "regimen": regimen, + "tipo_operacion": tipo_operacion, + "filename": filename + } + report = ReportDocument.objects.create( + user=request.user, + filters=filtros, + status='pending' + ) + generate_report_document.delay(report.id) + return Response({ + "report_id": report.id, + "status": report.status, + "created_at": report.created_at, + "download_url": report.file.url if report.file else None + }, status=202) - # Serializar pedimentos con documentos relacionados - results = [] - for ped in pedimentos: - ped_dict = { - 'aduana': ped.aduana, - 'patente': ped.patente, - 'regimen': ped.regimen, - 'pedimento': ped.pedimento, - 'pedimento_app': ped.pedimento_app, - 'clave_pedimento': ped.clave_pedimento, - 'tipo_operacion_id': ped.tipo_operacion_id, - 'contribuyente_id': ped.contribuyente_id, - 'documentos': [] +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def report_document_status(request, report_id): + try: + report = ReportDocument.objects.get(id=report_id, user=request.user) + data = { + "report_id": report.id, + "status": report.status, + "created_at": report.created_at, + "finished_at": report.finished_at, + "error_message": report.error_message, + "download_url": report.file.url if report.file else None } - # COVEs - for cove in Cove.objects.filter(pedimento=ped): - ped_dict['documentos'].append({ - 'tipo': 'COVE', - 'numero': cove.numero_cove, - 'estado': cove.cove_descargado, - 'acuse_estado': cove.acuse_cove_descargado - }) - # EDOCs - for edoc in EDocument.objects.filter(pedimento=ped): - ped_dict['documentos'].append({ - 'tipo': 'EDOC', - 'numero': edoc.numero_edocument, - 'estado': edoc.edocument_descargado, - 'acuse_estado': edoc.acuse_descargado, - }) - # PARTIDAS - for partida in Partida.objects.filter(pedimento=ped): - ped_dict['documentos'].append({ - 'tipo': 'PARTIDA', - 'numero': partida.numero_partida, - 'estado': partida.descargado - }) - results.append(ped_dict) + return Response(data) + except ReportDocument.DoesNotExist: + return Response({"error": "Reporte no encontrado"}, status=404) - if request.query_params.get('csv') == '1': - import csv - from django.http import HttpResponse - headers = [ - 'aduana', 'patente', 'regimen', 'pedimento', 'pedimento_app', 'clave_pedimento', - 'tipo_operacion_id', 'contribuyente_id', 'tipo_documento', 'numero_documento', 'estado', 'acuse_estado' - ] - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=table_summary.csv' - writer = csv.writer(response) - writer.writerow(headers) - # Llenar filas - if request.query_params.get('ultimos') == '1': - pedimentos = Pedimento.objects.filter(pedimentos_filters).order_by('-updated_at')[:100] - else: - pedimentos = Pedimento.objects.filter(pedimentos_filters) - for ped in pedimentos: - # COVEs - 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 - ]) - # EDOCs - 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 - ]) - # PARTIDAS - 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, '' - ]) - return response - # Si se solicita Excel, generar archivo y devolverlo - if request.query_params.get('excel') == '1': - import openpyxl - from openpyxl.utils import get_column_letter - from django.http import HttpResponse - wb = openpyxl.Workbook() - ws = wb.active - ws.title = "Resumen" - # Encabezados - headers = [ - 'aduana', 'patente', 'regimen', 'pedimento', 'pedimento_app', 'clave_pedimento', - 'tipo_operacion_id', 'contribuyente_id', 'tipo_documento', 'numero_documento', 'estado', 'acuse_estado' - ] - ws.append(headers) - # Llenar filas - for ped in results: - for doc in ped['documentos']: - ws.append([ - ped['aduana'], ped['patente'], ped['regimen'], ped['pedimento'], ped['pedimento_app'], - ped['clave_pedimento'], ped['tipo_operacion_id'], ped['contribuyente_id'], - doc.get('tipo'), doc.get('numero'), doc.get('estado'), doc.get('acuse_estado') - ]) - # Ajustar ancho de columnas - for i, col in enumerate(headers, 1): - ws.column_dimensions[get_column_letter(i)].width = max(12, len(col) + 2) - # Guardar en memoria y devolver como respuesta - from io import BytesIO - output = BytesIO() - wb.save(output) - output.seek(0) - response = HttpResponse( - output.read(), - content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - ) - response['Content-Disposition'] = 'attachment; filename=table_summary.xlsx' - return response - - # Aplicar paginación manual sobre results - paginator = CustomPagination() - page = paginator.paginate_queryset(results, request) - - return paginator.get_paginated_response({ - "results": page, - "filtros_aplicados": { - "organizacion_id": org_id, - "tipo_documento": tipo_documento, - "contribuyente__rfc": rfc, - "fecha_pago__gte": fecha_pago_gte, - "fecha_pago__lte": fecha_pago_lte, - "patente": patente, - "aduana": aduana, - "pedimento": pedimento, - "pedimento_app": pedimento_app, - "regimen": regimen, - "tipo_operacion": tipo_operacion +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def report_document_list(request): + reports = ReportDocument.objects.filter(user=request.user).order_by('-created_at') + data = [ + { + "report_id": r.id, + "status": r.status, + "created_at": r.created_at, + "finished_at": r.finished_at, + "error_message": r.error_message, + "download_url": r.file.url if r.file else None } - }) \ No newline at end of file + for r in reports + ] + return Response(data) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def report_document_download(request, report_id): + try: + report = ReportDocument.objects.get(id=report_id, user=request.user) + if not report.file: + return Response({"error": "El archivo aún no está disponible"}, status=404) + 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) \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index 9f2baff..78eb50f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -266,7 +266,7 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'America/Ojinaga' # Zona horaria de Cd. Juárez, Chihuahua +TIME_ZONE = 'America/Mexico_City' # Zona horaria de Cd. Juárez, Chihuahua USE_I18N = True USE_TZ = True