diff --git a/api/cuser/migrations/0004_alter_customuser_rfc.py b/api/cuser/migrations/0004_alter_customuser_rfc.py new file mode 100644 index 0000000..609384d --- /dev/null +++ b/api/cuser/migrations/0004_alter_customuser_rfc.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.3 on 2025-10-05 17:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cuser', '0003_alter_customuser_rfc'), + ('customs', '0015_partida_updated_at'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='rfc', + field=models.ForeignKey(blank=True, help_text='RFC associated with the user if they are an importer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='customs.importador'), + ), + ] diff --git a/api/cuser/models.py b/api/cuser/models.py index e8a1547..fae5055 100644 --- a/api/cuser/models.py +++ b/api/cuser/models.py @@ -12,7 +12,7 @@ class CustomUser(AbstractUser): profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True) is_importador = models.BooleanField(default=False, help_text="Indicates if the user is an importer") - rfc = models.CharField(max_length=1, null=True, blank=True, help_text="RFC of the user") + rfc = models.ForeignKey('customs.Importador', on_delete=models.SET_NULL, null=True, blank=True, related_name='users', help_text="RFC associated with the user if they are an importer") def __str__(self): return self.username diff --git a/api/reports/urls.py b/api/reports/urls.py index 78ed9cd..cbb04b0 100644 --- a/api/reports/urls.py +++ b/api/reports/urls.py @@ -6,7 +6,7 @@ from .views_table import table_summary 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('documentos-por-fecha/', documentos_por_fecha, name='documentos-por-fecha'), #path('table-summary/', table_summary, name='table-summary'), ] \ No newline at end of file diff --git a/api/reports/views.py b/api/reports/views.py index 8511d87..afcbb09 100644 --- a/api/reports/views.py +++ b/api/reports/views.py @@ -1,3 +1,4 @@ +from warnings import filters from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from api.customs.models import Pedimento, Cove, EDocument, Partida @@ -143,55 +144,50 @@ class ExportModelView(APIView): class ExportModelView(APIView): - my_tags = ['Reportes'] - permission_classes = [IsAuthenticated & ( - IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] + my_tags = ['Reportes'] + permission_classes = [IsAuthenticated & ( + IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] - @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter('model', openapi.IN_QUERY, description="Nombre del modelo (ejemplo: Registro500)", - type=openapi.TYPE_STRING, required=True) - ], - responses={200: openapi.Response('Campos disponibles', schema=openapi.Schema( - type=openapi.TYPE_OBJECT, - properties={ - 'fields': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_STRING)) - } - ))} - ) - def get(self, request, *args, **kwargs): - """ - Devuelve los campos disponibles para el modelo solicitado. - Ejemplo: /api/reports/exportmodel/?model=Registro500 - """ - model_name = request.query_params.get('model') - if not model_name: - return Response({'error': 'model is required'}, status=status.HTTP_400_BAD_REQUEST) - try: - model = apps.get_model('datastage', model_name) - except LookupError: - return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND) - fields = [f.name for f in model._meta.fields] - return Response({'fields': fields}) + @swagger_auto_schema(manual_parameters=[openapi.Parameter('model', openapi.IN_QUERY, description="Nombre del modelo (ejemplo: Registro500)", + type=openapi.TYPE_STRING, required=True) + ], + responses={200: openapi.Response('Campos disponibles', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'fields': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_STRING)) + } + ))} + ) + def get(self, request, *args, **kwargs): + """ + Devuelve los campos disponibles para el modelo solicitado. + Ejemplo: /api/reports/exportmodel/?model=Registro500 + """ + model_name = request.query_params.get('model') + if not model_name: + return Response({'error': 'model is required'}, status=status.HTTP_400_BAD_REQUEST) + try: + model = apps.get_model('datastage', model_name) + except LookupError: + return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND) + fields = [f.name for f in model._meta.fields] + return Response({'fields': fields}) - @swagger_auto_schema( - request_body=ExportModelSerializer, - responses={200: 'Archivo generado (Excel o CSV)'} - ) - def post(self, request, *args, **kwargs): - model_name = request.data.get('model') - fields = request.data.get('fields') - filters = request.data.get('filters', {}) - export_type = request.data.get('type', 'csv') + @swagger_auto_schema(request_body=ExportModelSerializer, esponses={200: 'Archivo generado (Excel o CSV)'}) + def post(self, request, *args, **kwargs): + model_name = request.data.get('model') + fields = request.data.get('fields') + filters = request.data.get('filters', {}) + filters['organizacion__id'] = self.request.user.organizacion.id if hasattr(request.user, 'organizacion') and request.user.organizacion else None + export_type = request.data.get('type', 'csv') + if not model_name or not fields: + return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST) - if not model_name or not fields: - return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST) - - module = request.data.get('module', 'datastage') - if export_type == 'excel': - return export_model_to_excel(request, model_name, fields, module, filters) - else: - return export_model_to_csv(request, model_name, fields, module, filters) + module = request.data.get('module', 'datastage') + if export_type == 'excel': + return export_model_to_excel(request, model_name, fields, module, filters) + else: + return export_model_to_csv(request, model_name, fields, module, filters) # Resumen general para dashboard @@ -256,29 +252,44 @@ def dashboard_summary(request): pedimento_ids = list(pedimentos_qs.values_list('id', flat=True)) coves_total = Cove.objects.filter(pedimento_id__in=pedimento_ids).count() - coves_procesados = Cove.objects.filter(pedimento_id__in=pedimento_ids, cove_descargado=True).count() - acuse_coves_procesados = Cove.objects.filter(pedimento_id__in=pedimento_ids, acuse_cove_descargado=True).count() + coves_procesados = Cove.objects.filter( + pedimento_id__in=pedimento_ids, cove_descargado=True).count() + acuse_coves_procesados = Cove.objects.filter( + pedimento_id__in=pedimento_ids, acuse_cove_descargado=True).count() acuse_coves_pendientes = coves_total - acuse_coves_procesados coves_pendientes = coves_total - coves_procesados - edocs_total = EDocument.objects.filter(pedimento_id__in=pedimento_ids).count() - edocs_descargados = EDocument.objects.filter(pedimento_id__in=pedimento_ids, edocument_descargado=True).count() - acuse_descargados = EDocument.objects.filter(pedimento_id__in=pedimento_ids, acuse_descargado=True).count() + edocs_total = EDocument.objects.filter( + pedimento_id__in=pedimento_ids).count() + edocs_descargados = EDocument.objects.filter( + pedimento_id__in=pedimento_ids, edocument_descargado=True).count() + acuse_descargados = EDocument.objects.filter( + pedimento_id__in=pedimento_ids, acuse_descargado=True).count() edocs_pendientes = edocs_total - edocs_descargados acuses_pendientes = edocs_total - acuse_descargados - remesas_total = Document.objects.filter(document_type__id=3, pedimento_id__in=pedimento_ids).count() - documentos_descargados = Document.objects.filter(pedimento_id__in=pedimento_ids).count() - partidas_total = Partida.objects.filter(pedimento_id__in=pedimento_ids).count() - partidas_descargadas = Partida.objects.filter(pedimento_id__in=pedimento_ids, descargado=True).count() + remesas_total = Document.objects.filter( + document_type__id=3, pedimento_id__in=pedimento_ids).count() + documentos_descargados = Document.objects.filter( + pedimento_id__in=pedimento_ids).count() + partidas_total = Partida.objects.filter( + pedimento_id__in=pedimento_ids).count() + partidas_descargadas = Partida.objects.filter( + pedimento_id__in=pedimento_ids, descargado=True).count() partidas_pendientes = partidas_total - partidas_descargadas # Indicadores de cumplimiento - cumplimiento_pedimentos = (pedimentos_completos / pedimentos_total * 100) if pedimentos_total else 0 - cumplimiento_acuse_coves = (acuse_coves_procesados / coves_total * 100) if coves_total else 0 - cumplimiento_coves = (coves_procesados / coves_total * 100) if coves_total else 0 - cumplimiento_edocs = (edocs_descargados / edocs_total * 100) if edocs_total else 0 - cumplimiento_acuses = (acuse_descargados / edocs_total * 100) if edocs_total else 0 - cumplimiento_partidas = (partidas_descargadas / partidas_total * 100) if partidas_total else 0 + cumplimiento_pedimentos = ( + pedimentos_completos / pedimentos_total * 100) if pedimentos_total else 0 + cumplimiento_acuse_coves = ( + acuse_coves_procesados / coves_total * 100) if coves_total else 0 + cumplimiento_coves = ( + coves_procesados / coves_total * 100) if coves_total else 0 + cumplimiento_edocs = (edocs_descargados / + edocs_total * 100) if edocs_total else 0 + cumplimiento_acuses = (acuse_descargados / + edocs_total * 100) if edocs_total else 0 + cumplimiento_partidas = (partidas_descargadas / + partidas_total * 100) if partidas_total else 0 return Response({ "pedimentos": { @@ -301,7 +312,7 @@ def dashboard_summary(request): "edocs_descargados": edocs_descargados, "edocs_pendientes": edocs_pendientes, "acuse_descargados": acuse_descargados, - "acuses_pendientes": acuses_pendientes, + "acuses_pendientes": acuses_pendientes, "edocs_cumplimiento": round(cumplimiento_edocs, 2), "acuses_cumplimiento": round(cumplimiento_acuses, 2) }, @@ -314,7 +325,7 @@ def dashboard_summary(request): "partidas": { "total": partidas_total, "partidas_descargadas": partidas_descargadas, - "partidas_pendientes": partidas_pendientes, - "cumplimiento": round(cumplimiento_partidas, 2) + "partidas_pendientes": partidas_pendientes, + "cumplimiento": round(cumplimiento_partidas, 2) } - }) \ No newline at end of file + }) diff --git a/api/reports/views_new.py b/api/reports/views_new.py new file mode 100644 index 0000000..beed2e0 --- /dev/null +++ b/api/reports/views_new.py @@ -0,0 +1,278 @@ +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from api.customs.models import Pedimento, Cove, EDocument, Partida +from api.record.models import Document +from api.organization.models import Organizacion +from django.db.models import Count, Q +import csv +import io +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from .serializers import ExportModelSerializer +from rest_framework.response import Response +from django.http import HttpResponse +import openpyxl +from django.apps import apps +from rest_framework import status +from django.shortcuts import render +from rest_framework import viewsets +from rest_framework.views import APIView +from core.permissions import ( + IsSameOrganization, + IsSameOrganizationDeveloper, + IsSameOrganizationAndAdmin, + IsSuperUser +) + + +def export_model_to_csv(request, model_name, fields, module='datastage', filters=None): + model = apps.get_model(module, model_name) + queryset = model.objects.filter(**(filters or {})).values(*fields) + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename="{model_name}.csv"' + writer = csv.DictWriter(response, fieldnames=fields) + writer.writeheader() + for row in queryset: + writer.writerow(row) + return response + + +def export_model_to_excel(request, model_name, fields, module='datastage', filters=None): + model = apps.get_model(module, model_name) + queryset = model.objects.filter(**(filters or {})).values(*fields) + wb = openpyxl.Workbook() + ws = wb.active + ws.append(fields) + for row in queryset: + # Convertir cada valor a string para asegurar compatibilidad con Excel + row_values = [] + for field in fields: + value = row[field] + # Si es UUID u otro objeto, convertirlo a string + if hasattr(value, '__str__'): + value = str(value) + row_values.append(value) + ws.append(row_values) + output = io.BytesIO() + wb.save(output) + output.seek(0) + response = HttpResponse( + output.read(), + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = f'attachment; filename="{model_name}.xlsx"' + return response + + +class ExportModelView(APIView): + my_tags = ['Reportes'] + permission_classes = [IsAuthenticated & ( + IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser + )] + + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'model', + openapi.IN_QUERY, + description="Nombre del modelo (ejemplo: Registro500)", + type=openapi.TYPE_STRING, + required=True + ) + ], + responses={200: openapi.Response('Campos disponibles', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'fields': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Items(type=openapi.TYPE_STRING) + ) + } + ))} + ) + def get(self, request, *args, **kwargs): + """ + Devuelve los campos disponibles para el modelo solicitado. + Ejemplo: /api/reports/exportmodel/?model=Registro500 + """ + model_name = request.query_params.get('model') + module = request.query_params.get('module', 'datastage') + if not model_name: + return Response({'error': 'model is required'}, status=status.HTTP_400_BAD_REQUEST) + try: + model = apps.get_model(module, model_name) + except LookupError: + return Response( + {'error': f'Model {model_name} not found in app {module}'}, + status=status.HTTP_404_NOT_FOUND + ) + fields = [f.name for f in model._meta.fields] + return Response({'fields': fields}) + + @swagger_auto_schema( + request_body=ExportModelSerializer, + responses={200: 'Archivo generado (Excel o CSV)'} + ) + def post(self, request, *args, **kwargs): + model_name = request.data.get('model') + fields = request.data.get('fields') + export_type = request.data.get('type', 'csv') + module = request.data.get('module', 'datastage') + + if not model_name or not fields: + return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST) + + # Aplicar filtro de organización + filters = {"organizacion__id": request.user.organizacion.id} + # Agregar filtros adicionales del request + filters.update(request.data.get('filters', {})) + + if export_type == 'excel': + return export_model_to_excel(request, model_name, fields, module, filters) + else: + return export_model_to_csv(request, model_name, fields, module, filters) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def dashboard_summary(request): + org_id = request.query_params.get('organizacion_id') + filters = {} + user = request.user + + pedimento_app = request.query_params.get('pedimento_app') + aduana = request.query_params.get('aduana') + patente = request.query_params.get('patente') + regimen = request.query_params.get('regimen') + agente_aduanal = request.query_params.get('agente_aduanal') + tipo_operacion = request.query_params.get('tipo_operacion') + fecha_pago_gte = request.query_params.get('fecha_pago__gte') + fecha_pago_lte = request.query_params.get('fecha_pago__lte') + contribuyente__rfc = request.query_params.get('contribuyente__rfc') + + # Si no se especifica organización y el usuario tiene organización, usarla + if not org_id and hasattr(user, 'organizacion') and user.organizacion: + org_id = user.organizacion.id + # Si no es superusuario, filtrar por organización + if org_id and not getattr(user, 'is_superuser', False): + filters['organizacion_id'] = org_id + + # Si el usuario pertenece al grupo Importador, filtrar por RFC + if user.groups.filter(name='Importador').exists(): + rfc = getattr(user, 'rfc', None) + if rfc: + filters['contribuyente__rfc'] = rfc + + if pedimento_app: + filters['pedimento_app'] = pedimento_app + if aduana: + filters['aduana'] = aduana + if patente: + filters['patente'] = patente + if regimen: + filters['regimen'] = regimen + if agente_aduanal: + filters['agente_aduanal'] = agente_aduanal + if tipo_operacion: + filters['tipo_operacion__tipo'] = tipo_operacion + if fecha_pago_gte: + filters['fecha_pago__gte'] = fecha_pago_gte + if fecha_pago_lte: + filters['fecha_pago__lte'] = fecha_pago_lte + if contribuyente__rfc: + filters['contribuyente__rfc'] = contribuyente__rfc + + # Filtrar pedimentos + pedimentos_qs = Pedimento.objects.filter(**filters) + pedimentos_total = pedimentos_qs.count() + pedimentos_completos = pedimentos_qs.filter(existe_expediente=True).count() + pedimentos_pendientes = pedimentos_total - pedimentos_completos + + # Usar los IDs de pedimentos filtrados para los demás modelos + pedimento_ids = list(pedimentos_qs.values_list('id', flat=True)) + + coves_total = Cove.objects.filter(pedimento_id__in=pedimento_ids).count() + coves_procesados = Cove.objects.filter( + pedimento_id__in=pedimento_ids, + cove_descargado=True + ).count() + acuse_coves_procesados = Cove.objects.filter( + pedimento_id__in=pedimento_ids, + acuse_cove_descargado=True + ).count() + acuse_coves_pendientes = coves_total - acuse_coves_procesados + coves_pendientes = coves_total - coves_procesados + + edocs_total = EDocument.objects.filter(pedimento_id__in=pedimento_ids).count() + edocs_descargados = EDocument.objects.filter( + pedimento_id__in=pedimento_ids, + edocument_descargado=True + ).count() + acuse_descargados = EDocument.objects.filter( + pedimento_id__in=pedimento_ids, + acuse_descargado=True + ).count() + edocs_pendientes = edocs_total - edocs_descargados + acuses_pendientes = edocs_total - acuse_descargados + + remesas_total = Document.objects.filter( + document_type__id=3, + pedimento_id__in=pedimento_ids + ).count() + documentos_descargados = Document.objects.filter( + pedimento_id__in=pedimento_ids + ).count() + + partidas_total = Partida.objects.filter(pedimento_id__in=pedimento_ids).count() + partidas_descargadas = Partida.objects.filter( + pedimento_id__in=pedimento_ids, + descargado=True + ).count() + partidas_pendientes = partidas_total - partidas_descargadas + + # Indicadores de cumplimiento + cumplimiento_pedimentos = (pedimentos_completos / pedimentos_total * 100) if pedimentos_total else 0 + cumplimiento_acuse_coves = (acuse_coves_procesados / coves_total * 100) if coves_total else 0 + cumplimiento_coves = (coves_procesados / coves_total * 100) if coves_total else 0 + cumplimiento_edocs = (edocs_descargados / edocs_total * 100) if edocs_total else 0 + cumplimiento_acuses = (acuse_descargados / edocs_total * 100) if edocs_total else 0 + cumplimiento_partidas = (partidas_descargadas / partidas_total * 100) if partidas_total else 0 + + return Response({ + "pedimentos": { + "total": pedimentos_total, + "completos": pedimentos_completos, + "pendientes": pedimentos_pendientes, + "cumplimiento": round(cumplimiento_pedimentos, 2) + }, + "coves": { + "total": coves_total, + "coves_procesados": coves_procesados, + "acuse_coves_procesados": acuse_coves_procesados, + "coves_pendientes": coves_pendientes, + "acuse_coves_pendientes": acuse_coves_pendientes, + "coves_cumplimiento": round(cumplimiento_coves, 2), + "acuse_coves_cumplimiento": round(cumplimiento_acuse_coves, 2) + }, + "edocuments": { + "total": edocs_total, + "edocs_descargados": edocs_descargados, + "edocs_pendientes": edocs_pendientes, + "acuse_descargados": acuse_descargados, + "acuses_pendientes": acuses_pendientes, + "edocs_cumplimiento": round(cumplimiento_edocs, 2), + "acuses_cumplimiento": round(cumplimiento_acuses, 2) + }, + "remesas": { + "total": remesas_total + }, + "documentos": { + "descargados": documentos_descargados + }, + "partidas": { + "total": partidas_total, + "partidas_descargadas": partidas_descargadas, + "partidas_pendientes": partidas_pendientes, + "cumplimiento": round(cumplimiento_partidas, 2) + } + }) \ No newline at end of file diff --git a/mixins/filtrado_organizacion.py b/mixins/filtrado_organizacion.py index d232d1b..be52701 100644 --- a/mixins/filtrado_organizacion.py +++ b/mixins/filtrado_organizacion.py @@ -5,7 +5,7 @@ class FiltroPorOrganizacionMixin: model = None campo_usuario = 'user' campo_organizacion = 'organizacion' - campo_rfc = 'rfc' + campo_rfc = 'rfc__id' campo_contribuyente = 'pedimento__contribuyente' # solo si aplica def get_queryset_filtrado(self): @@ -61,10 +61,10 @@ class OrganizacionFiltradaMixin: if 'Agente Aduanal' in grupos: return model.objects.filter(**filtros_base) - if hasattr(model, self.campo_contribuyente): - if self.request.user.is_authenticated and'Importador' in grupos and getattr(self.request.user, 'is_importador', False): - filtros_base[f"{self.campo_contribuyente}"] = self.request.user.rfc - return model.objects.filter(**filtros_base) + # if hasattr(model, self.campo_contribuyente): + if self.request.user.is_authenticated and 'Importador' in grupos : + filtros_base[f"{self.campo_contribuyente}__rfc"] = self.request.user.rfc.rfc + return model.objects.filter(**filtros_base) # Si no entra en los roles válidos return model.objects.none()