Compare commits
4 Commits
feature/ba
...
fix/forzar
| Author | SHA1 | Date | |
|---|---|---|---|
| 94846fec8a | |||
| e378f2d949 | |||
| a318b70324 | |||
| 9bbed42cf3 |
@@ -8,10 +8,9 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from core.permissions import (
|
from core.permissions import (
|
||||||
IsSameOrganization,
|
get_org_context,
|
||||||
IsSameOrganizationDeveloper,
|
require_permission,
|
||||||
IsSameOrganizationAndAdmin,
|
user_has_permission,
|
||||||
IsSuperUser
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from api.organization.models import UsoAlmacenamiento, Organizacion
|
from api.organization.models import UsoAlmacenamiento, Organizacion
|
||||||
@@ -34,7 +33,7 @@ class DocumentUtilInformation(LoggingMixin, APIView, FiltroPorOrganizacionMixin)
|
|||||||
View to get the total storage used by the organization and stats of documents added in last 1, 7, and 30 days.
|
View to get the total storage used by the organization and stats of documents added in last 1, 7, and 30 days.
|
||||||
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
|
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('cards.view')]
|
||||||
model = Document
|
model = Document
|
||||||
|
|
||||||
my_tags = ['Cards']
|
my_tags = ['Cards']
|
||||||
@@ -100,7 +99,7 @@ class ViewPedimentoServicesUtilInformation(LoggingMixin, APIView, FiltroPorOrgan
|
|||||||
View para obtener información de uso de servicios relacionados con pedimentos.
|
View para obtener información de uso de servicios relacionados con pedimentos.
|
||||||
Devuelve la cantidad de procesos por estado (1: espera, 2: proceso, 3: finalizado, 4: error) para la organización.
|
Devuelve la cantidad de procesos por estado (1: espera, 2: proceso, 3: finalizado, 4: error) para la organización.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('cards.view')]
|
||||||
model = Document
|
model = Document
|
||||||
my_tags = ['Cards']
|
my_tags = ['Cards']
|
||||||
|
|
||||||
@@ -140,29 +139,17 @@ class ViewPedimentoServicesUtilInformation(LoggingMixin, APIView, FiltroPorOrgan
|
|||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Si es super usuario, devuelve todos los procesos
|
org = get_org_context(self.request.user)
|
||||||
if self.request.user.is_superuser:
|
if not org:
|
||||||
return ProcesamientoPedimento.objects.all()
|
return ProcesamientoPedimento.objects.none()
|
||||||
|
|
||||||
# Si es Administrador de la organizacion devuelve todos los servicios de la organizacion
|
if self.request.user.is_importador:
|
||||||
if self.request.user.is_authenticated and self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
return ProcesamientoPedimento.objects.filter(
|
||||||
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=self.request.user.organizacion)
|
pedimento__organizacion=org,
|
||||||
|
pedimento__contribuyente__in=self.request.user.rfc.all(),
|
||||||
|
)
|
||||||
|
|
||||||
# Si es Desarrollador de la organizacion devuelve todos los servicios de la organizacion
|
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=org)
|
||||||
if self.request.user.is_authenticated and self.request.user.groups.filter(name='developer').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
|
||||||
return self.request.user.organizacion.procesamiento_pedimentos.all()
|
|
||||||
|
|
||||||
if self.request.user.is_authenticated and self.request.user.groups.filter(name='user').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
|
||||||
return self.request.user.organizacion.procesamiento_pedimentos.all()
|
|
||||||
|
|
||||||
# Si es importador de la organizacion, devuelve los servicios relacionados con sus pedimentos
|
|
||||||
if self.request.user.is_authenticated and self.request.user.groups.filter(name='importador').exists() and self.request.user.is_importador and self.request.user.groups.filter(name='user').exists():
|
|
||||||
return self.request.user.organizacion.procesamiento_pedimentos.filter(pedimento__contribuyente__in=self.request.user.rfc.all())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Si es parte de una organización, filtrar por esa organización
|
|
||||||
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=self.request.user.organizacion)
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
@@ -193,12 +180,21 @@ class UserActivityAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
|
|||||||
Endpoint para análisis de actividades de usuario.
|
Endpoint para análisis de actividades de usuario.
|
||||||
Devuelve el conteo de acciones por tipo y los 5 usuarios más activos.
|
Devuelve el conteo de acciones por tipo y los 5 usuarios más activos.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('cards.view')]
|
||||||
|
|
||||||
model = UserActivity
|
model = UserActivity
|
||||||
|
campo_organizacion = 'user__organizacion'
|
||||||
|
|
||||||
my_tags = ['Cards']
|
my_tags = ['Cards']
|
||||||
|
|
||||||
|
def get_queryset_importador(self):
|
||||||
|
# Importadores solo ven sus propias actividades
|
||||||
|
user = self.request.user
|
||||||
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return UserActivity.objects.none()
|
||||||
|
return UserActivity.objects.filter(user__organizacion=org, user=user)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Get analysis of user activities. Permite filtrar por fecha de actividades.",
|
operation_description="Get analysis of user activities. Permite filtrar por fecha de actividades.",
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
@@ -253,7 +249,9 @@ class UserActivityAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.get_queryset_filtrado()
|
if self.request.user.is_importador:
|
||||||
|
return self.get_queryset_importador()
|
||||||
|
return self.get_queryset_filtrado()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
@@ -289,11 +287,20 @@ class RequestLogAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
|
|||||||
Endpoint para análisis de logs de peticiones.
|
Endpoint para análisis de logs de peticiones.
|
||||||
Devuelve el conteo por método, los paths más solicitados y el promedio de tiempo de respuesta.
|
Devuelve el conteo por método, los paths más solicitados y el promedio de tiempo de respuesta.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('cards.view')]
|
||||||
model = RequestLog
|
model = RequestLog
|
||||||
|
campo_organizacion = 'user__organizacion'
|
||||||
|
|
||||||
my_tags = ['Cards']
|
my_tags = ['Cards']
|
||||||
|
|
||||||
|
def get_queryset_importador(self):
|
||||||
|
# Importadores solo ven sus propios logs
|
||||||
|
user = self.request.user
|
||||||
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return RequestLog.objects.none()
|
||||||
|
return RequestLog.objects.filter(user__organizacion=org, user=user)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Get analysis of request logs. Permite filtrar por fecha de logs.",
|
operation_description="Get analysis of request logs. Permite filtrar por fecha de logs.",
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
@@ -345,6 +352,8 @@ class RequestLogAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if self.request.user.is_importador:
|
||||||
|
return self.get_queryset_importador()
|
||||||
return self.get_queryset_filtrado()
|
return self.get_queryset_filtrado()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
@@ -376,7 +385,7 @@ class LastDocumentView(LoggingMixin, APIView, DocumentosFiltradosMixin):
|
|||||||
View que obtiene los ultimos 10 documentos agregados.
|
View que obtiene los ultimos 10 documentos agregados.
|
||||||
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
|
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('cards.view')]
|
||||||
model = Document
|
model = Document
|
||||||
|
|
||||||
my_tags = ['Cards']
|
my_tags = ['Cards']
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
# Fieldsets para editar un usuario
|
# Fieldsets para editar un usuario
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
('Información personal', {'fields': ('first_name', 'last_name', 'email', 'organizacion', 'profile_picture', 'is_importador', 'rfc')}),
|
('Información personal', {'fields': ('first_name', 'last_name', 'email', 'organizacion', 'active_organization', 'profile_picture', 'is_importador', 'rfc')}),
|
||||||
('Permisos', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
('Permisos', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
||||||
('Fechas importantes', {'fields': ('last_login', 'date_joined')}),
|
('Fechas importantes', {'fields': ('last_login', 'date_joined')}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ class CustomUser(AbstractUser):
|
|||||||
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, null=True, blank=True, related_name='users')
|
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, null=True, blank=True, related_name='users')
|
||||||
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
|
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
|
||||||
|
|
||||||
|
# Contexto de trabajo activo para superusuarios. Filtra datos igual que un usuario normal.
|
||||||
|
# Sin este campo activo, el superuser no puede consultar datos — debe hacer switch primero.
|
||||||
|
active_organization = models.ForeignKey(
|
||||||
|
'organization.Organizacion',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='superusers_activos',
|
||||||
|
help_text="Solo superusuarios: organización activa para contexto de trabajo",
|
||||||
|
)
|
||||||
|
|
||||||
is_importador = models.BooleanField(default=False, help_text="Indicates if the user is an importer")
|
is_importador = models.BooleanField(default=False, help_text="Indicates if the user is an importer")
|
||||||
rfc = models.ManyToManyField('customs.Importador', blank=True, related_name='users', help_text="RFCs de importadores asociados al usuario")
|
rfc = models.ManyToManyField('customs.Importador', blank=True, related_name='users', help_text="RFCs de importadores asociados al usuario")
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class CustomUserSerializer(serializers.ModelSerializer):
|
|||||||
Serializer for the CustomUser model.
|
Serializer for the CustomUser model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
password = serializers.CharField(write_only=True)
|
password = serializers.CharField(write_only=True, required=False)
|
||||||
groups = serializers.PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
|
groups = serializers.PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
|
||||||
rfc = serializers.PrimaryKeyRelatedField(
|
rfc = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=Importador.objects.all(),
|
queryset=Importador.objects.all(),
|
||||||
@@ -23,6 +23,17 @@ class CustomUserSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'password', 'profile_picture', 'organizacion', 'is_importador', 'rfc', 'is_active', 'is_superuser', 'groups']
|
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'password', 'profile_picture', 'organizacion', 'is_importador', 'rfc', 'is_active', 'is_superuser', 'groups']
|
||||||
read_only_fields = ['id', 'organizacion', 'is_superuser']
|
read_only_fields = ['id', 'organizacion', 'is_superuser']
|
||||||
|
|
||||||
|
def validate_password(self, value):
|
||||||
|
if not value or not value.strip():
|
||||||
|
raise serializers.ValidationError("La contraseña no puede estar vacía o contener solo espacios.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
# En create, la contraseña es obligatoria
|
||||||
|
if self.instance is None and not attrs.get('password'):
|
||||||
|
raise serializers.ValidationError({"password": "Este campo es requerido."})
|
||||||
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
groups = validated_data.pop('groups', [])
|
groups = validated_data.pop('groups', [])
|
||||||
rfcs = validated_data.pop('rfc', [])
|
rfcs = validated_data.pop('rfc', [])
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ from core.permissions import (
|
|||||||
IsSameOrganization,
|
IsSameOrganization,
|
||||||
IsSameOrganizationDeveloper,
|
IsSameOrganizationDeveloper,
|
||||||
IsSameOrganizationAndAdmin,
|
IsSameOrganizationAndAdmin,
|
||||||
IsSuperUser
|
IsSuperUser,
|
||||||
|
get_org_context,
|
||||||
|
is_internal_service_request,
|
||||||
|
user_has_permission,
|
||||||
|
require_permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .serializers import CustomUserSerializer
|
from .serializers import CustomUserSerializer
|
||||||
@@ -74,78 +78,62 @@ class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
|||||||
"""
|
"""
|
||||||
ViewSet for CustomUser model.
|
ViewSet for CustomUser model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSameOrganization )]
|
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
serializer_class = CustomUserSerializer
|
serializer_class = CustomUserSerializer
|
||||||
filterset_fields = ['username', 'email', 'first_name', 'last_name', 'organizacion', 'is_importador']
|
filterset_fields = ['username', 'email', 'first_name', 'last_name', 'organizacion', 'is_importador']
|
||||||
my_tags = ['User Profile']
|
my_tags = ['User Profile']
|
||||||
|
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
# Permitir eliminar usuarios solo a admin, Agente Aduanal y user de la misma organización
|
if self.action in ('me', 'change_password'):
|
||||||
if self.action == 'destroy':
|
return [IsAuthenticated()]
|
||||||
user = self.request.user
|
perms = {
|
||||||
if not (
|
'list': 'usuarios.view',
|
||||||
user.is_superuser or
|
'retrieve': 'usuarios.view',
|
||||||
user.groups.filter(name='admin').exists() or
|
'create': 'usuarios.create',
|
||||||
user.groups.filter(name='Agente Aduanal').exists() or
|
'update': 'usuarios.edit',
|
||||||
user.groups.filter(name='user').exists()
|
'partial_update': 'usuarios.edit',
|
||||||
):
|
'destroy': 'usuarios.delete',
|
||||||
from rest_framework.exceptions import PermissionDenied
|
}
|
||||||
raise PermissionDenied("Solo admin, Agente Aduanal o user pueden eliminar usuarios.")
|
codename = perms.get(self.action, 'usuarios.view')
|
||||||
elif self.action in ['create', 'update', 'partial_update']:
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
if not (self.request.user.is_superuser or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='Importador').exists()) :
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
raise PermissionDenied("Solo admin o superusuario pueden modificar usuarios.")
|
|
||||||
return super().get_permissions()
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
# Solo permitir eliminar usuarios de la misma organización
|
user = self.request.user
|
||||||
if self.request.user.is_superuser or instance.organizacion == self.request.user.organizacion:
|
org = get_org_context(user)
|
||||||
|
if user.is_superuser or instance.organizacion == org:
|
||||||
instance.delete()
|
instance.delete()
|
||||||
else:
|
else:
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
raise PermissionDenied("Solo puedes eliminar usuarios de tu organización.")
|
raise PermissionDenied("Solo puedes eliminar usuarios de tu organización.")
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Si es importador, solo puede ver su propio usuario
|
user = self.request.user
|
||||||
if self.request.user.groups.filter(name='importador').exists() or self.request.user.groups.filter(name='Importador').exists():
|
if is_internal_service_request(self.request):
|
||||||
return CustomUser.objects.filter(pk=self.request.user.pk)
|
return CustomUser.objects.all()
|
||||||
|
if not user_has_permission(user, 'usuarios.view'):
|
||||||
# Otros roles: filtrar por organización
|
return CustomUser.objects.none()
|
||||||
return self.get_queryset_filtrado_por_organizacion()
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return CustomUser.objects.none()
|
||||||
|
return CustomUser.objects.filter(organizacion=org)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
# Always assign the creator's organization
|
creator = self.request.user
|
||||||
if self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
|
||||||
if not self.request.user.organizacion:
|
|
||||||
raise PermissionDenied("Los administradores deben tener una organización asignada para crear usuarios.")
|
|
||||||
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
|
|
||||||
send_activation_email(user, self.request) # Usa template HTML
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
if creator.is_superuser:
|
||||||
# If superuser, allow creating users without organization
|
|
||||||
user = serializer.save(is_active=False)
|
user = serializer.save(is_active=False)
|
||||||
send_activation_email(user, self.request) # Usa template HTML
|
send_activation_email(user, self.request)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.request.user.groups.filter(name='developer').exists():
|
if creator.is_importador:
|
||||||
# Developers can create users but must assign an organization
|
|
||||||
if not self.request.user.organizacion:
|
|
||||||
raise PermissionDenied("Los desarrolladores deben tener una organización asignada para crear usuarios.")
|
|
||||||
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
|
|
||||||
send_activation_email(user, self.request) # Usa template HTML
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.request.user.groups.filter(name='importador').exists():
|
|
||||||
# No puedes crear un usuario si eres importador
|
|
||||||
raise PermissionDenied("Los importadores no pueden crear usuarios.")
|
raise PermissionDenied("Los importadores no pueden crear usuarios.")
|
||||||
|
|
||||||
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
|
org = get_org_context(creator)
|
||||||
send_activation_email(user, self.request) # Usa template HTML
|
if not org:
|
||||||
return
|
raise PermissionDenied("Debes tener una organización asignada para crear usuarios.")
|
||||||
|
|
||||||
|
user = serializer.save(organizacion=org, is_active=False)
|
||||||
|
send_activation_email(user, self.request)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
||||||
def me(self, request):
|
def me(self, request):
|
||||||
@@ -167,8 +155,11 @@ class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
|||||||
"""
|
"""
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
current_user = request.user
|
current_user = request.user
|
||||||
# Solo el propio usuario, admin o superuser pueden cambiar la contraseña
|
puede_cambiar_ajena = (
|
||||||
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists() or user == current_user):
|
current_user.is_superuser or
|
||||||
|
user_has_permission(current_user, 'usuarios.change_password')
|
||||||
|
)
|
||||||
|
if not (puede_cambiar_ajena or user == current_user):
|
||||||
raise PermissionDenied("No tienes permiso para cambiar la contraseña de este usuario.")
|
raise PermissionDenied("No tienes permiso para cambiar la contraseña de este usuario.")
|
||||||
|
|
||||||
old_password = request.data.get('old_password')
|
old_password = request.data.get('old_password')
|
||||||
@@ -176,8 +167,7 @@ class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
|||||||
if not new_password:
|
if not new_password:
|
||||||
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
|
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
|
||||||
|
|
||||||
# Si no es admin/superuser, debe validar old_password
|
if not puede_cambiar_ajena:
|
||||||
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists()):
|
|
||||||
if not old_password or not user.check_password(old_password):
|
if not old_password or not user.check_password(old_password):
|
||||||
return Response({'detail': 'La contraseña actual es incorrecta.'}, status=400)
|
return Response({'detail': 'La contraseña actual es incorrecta.'}, status=400)
|
||||||
|
|
||||||
@@ -226,11 +216,11 @@ class ProfilePictureView(LoggingMixin, APIView):
|
|||||||
my_tags = ['User Profile']
|
my_tags = ['User Profile']
|
||||||
|
|
||||||
def get(self, request, user_id):
|
def get(self, request, user_id):
|
||||||
# Obtiene el usuario (automáticamente 404 si no existe)
|
|
||||||
user = get_object_or_404(CustomUser, pk=user_id)
|
user = get_object_or_404(CustomUser, pk=user_id)
|
||||||
|
|
||||||
# El permiso IsOwnerOrAdmin ya verificó que request.user == user o es admin
|
org = get_org_context(request.user)
|
||||||
# Así que no necesitas validar manualmente los permisos aquí.
|
if not request.user.is_superuser and user.organizacion != org:
|
||||||
|
raise Http404("No autorizado")
|
||||||
|
|
||||||
if not user.profile_picture:
|
if not user.profile_picture:
|
||||||
raise Http404("El usuario no tiene imagen de perfil")
|
raise Http404("El usuario no tiene imagen de perfil")
|
||||||
@@ -267,6 +257,8 @@ class PasswordResetConfirmView(APIView):
|
|||||||
return Response({'detail': 'Enlace inválido.'}, status=400)
|
return Response({'detail': 'Enlace inválido.'}, status=400)
|
||||||
if not default_token_generator.check_token(user, token):
|
if not default_token_generator.check_token(user, token):
|
||||||
return Response({'detail': 'Token inválido o expirado.'}, status=400)
|
return Response({'detail': 'Token inválido o expirado.'}, status=400)
|
||||||
|
if not user.is_active:
|
||||||
|
return Response({'detail': 'La cuenta de usuario no está activa.'}, status=400)
|
||||||
password = request.data.get('password')
|
password = request.data.get('password')
|
||||||
if not password:
|
if not password:
|
||||||
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
|
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ class EDocumentSerializer(serializers.ModelSerializer):
|
|||||||
numero = str(obj.numero_edocument).strip()
|
numero = str(obj.numero_edocument).strip()
|
||||||
# id_pedimento = str(obj.pedimento_id).strip()
|
# id_pedimento = str(obj.pedimento_id).strip()
|
||||||
|
|
||||||
# excluir e documents de tipo request y de tipo error
|
# excluir solo request (21, 25); errores (22, 26) se incluyen para detección en frontend
|
||||||
qs = Document.objects.filter(
|
qs = Document.objects.filter(
|
||||||
pedimento=obj.pedimento,
|
pedimento=obj.pedimento,
|
||||||
archivo__icontains=numero,
|
archivo__icontains=numero,
|
||||||
@@ -240,15 +240,11 @@ class CoveSerializer(serializers.ModelSerializer):
|
|||||||
try:
|
try:
|
||||||
numero = str(obj.numero_cove).strip()
|
numero = str(obj.numero_cove).strip()
|
||||||
|
|
||||||
# Excluir los tipo de documento 20, 24, 23 y 19
|
# Excluir solo request (19, 23); errores (20, 24) se incluyen para detección en frontend
|
||||||
# 20 = error solicitud cove
|
|
||||||
# 24 = error solicitud acuse cove
|
|
||||||
# 23 = request acuse cove
|
|
||||||
# 19 = request cove
|
|
||||||
qs = Document.objects.filter(
|
qs = Document.objects.filter(
|
||||||
pedimento=obj.pedimento,
|
pedimento=obj.pedimento,
|
||||||
archivo__icontains=numero,
|
archivo__icontains=numero,
|
||||||
).exclude(document_type_id__in=[20, 24, 23, 19])
|
).exclude(document_type_id__in=[19, 23])
|
||||||
|
|
||||||
# Filtro por organización si aplica
|
# Filtro por organización si aplica
|
||||||
if hasattr(obj, 'organizacion') and obj.organizacion:
|
if hasattr(obj, 'organizacion') and obj.organizacion:
|
||||||
|
|||||||
@@ -91,12 +91,18 @@ def procesar_coves_pedimento(pedimento_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/coves",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/coves",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio de COVEs enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando COVEs para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_acuse_coves_pedimento(pedimento_id):
|
def procesar_acuse_coves_pedimento(pedimento_id):
|
||||||
@@ -114,12 +120,18 @@ def procesar_acuse_coves_pedimento(pedimento_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio de acuses de COVEs enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"Acuses de COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando acuses de COVEs para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_edocs_pedimento(pedimento_id):
|
def procesar_edocs_pedimento(pedimento_id):
|
||||||
@@ -137,12 +149,18 @@ def procesar_edocs_pedimento(pedimento_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio de E-documents enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"E-documents encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando E-documents para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_acuses_pedimento(pedimento_id):
|
def procesar_acuses_pedimento(pedimento_id):
|
||||||
@@ -160,12 +178,18 @@ def procesar_acuses_pedimento(pedimento_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio de acuses enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"Acuses encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando acuses para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_partidas_pedimento(pedimento_id):
|
def procesar_partidas_pedimento(pedimento_id):
|
||||||
@@ -177,18 +201,31 @@ def procesar_partidas_pedimento(pedimento_id):
|
|||||||
).first()
|
).first()
|
||||||
credenciales_dict = credenciales_to_dict(credenciales)
|
credenciales_dict = credenciales_to_dict(credenciales)
|
||||||
|
|
||||||
|
partidas_pendientes = list(pedimento.partidas.filter(descargado=False))
|
||||||
payload = {
|
payload = {
|
||||||
"partidas": [partida_to_dict(partida) for partida in pedimento.partidas.filter(descargado=False)],
|
"partidas": [partida_to_dict(p) for p in partidas_pendientes],
|
||||||
"pedimento": pedimento_dict,
|
"pedimento": pedimento_dict,
|
||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/partidas/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/partidas/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio de partidas enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
logging.info(
|
||||||
|
f"Partidas encoladas para pedimento {pedimento.pedimento}: "
|
||||||
|
f"{result.get('total', 0)} de {len(partidas_pendientes)}"
|
||||||
|
)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(
|
||||||
|
f"Error encolando partidas para pedimento {pedimento.pedimento}: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_remesas_pedimento(pedimento_id):
|
def procesar_remesas_pedimento(pedimento_id):
|
||||||
@@ -205,12 +242,18 @@ def procesar_remesas_pedimento(pedimento_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/remesas",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/remesas",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio de remesas enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"Remesa encolada para pedimento {pedimento.pedimento}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando remesa para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_pedimento_completo_individual(pedimento_id):
|
def procesar_pedimento_completo_individual(pedimento_id):
|
||||||
@@ -225,13 +268,19 @@ def procesar_pedimento_completo_individual(pedimento_id):
|
|||||||
"pedimento": pedimento_dict,
|
"pedimento": pedimento_dict,
|
||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/pedimento_completo",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/pedimento_completo",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
timeout=60
|
||||||
return response
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"Pedimento completo encolado: {pedimento.pedimento}")
|
||||||
|
return response
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando pedimento completo {pedimento.pedimento}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_pedimentos_completos(organizacion_id):
|
def procesar_pedimentos_completos(organizacion_id):
|
||||||
@@ -270,13 +319,18 @@ def procesar_pedimentos_completos(organizacion_id):
|
|||||||
url = f"{SERVICE_API_URL_V2}/services/pedimento_completo"
|
url = f"{SERVICE_API_URL_V2}/services/pedimento_completo"
|
||||||
dataJson = json.dumps(payload)
|
dataJson = json.dumps(payload)
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
url,
|
response = requests.post(
|
||||||
data=dataJson,
|
url,
|
||||||
headers={"Content-Type": "application/json"}
|
data=dataJson,
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
timeout=60
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logging.info(f"Pedimento completo encolado: {pedimento.pedimento}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando pedimento completo {pedimento.pedimento}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_remesas(organizacion_id):
|
def procesar_remesas(organizacion_id):
|
||||||
@@ -311,9 +365,11 @@ def procesar_remesas(organizacion_id):
|
|||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{SERVICE_API_URL_V2}/services/remesas/",
|
f"{SERVICE_API_URL_V2}/services/remesas/",
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
headers={"Content-Type": "application/json"}
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=60
|
||||||
)
|
)
|
||||||
logger.info(f"Servicio enviado para pedimento {pedimento.pedimento} — status {response.status_code}")
|
response.raise_for_status()
|
||||||
|
logger.info(f"Remesa encolada para pedimento {pedimento.pedimento} — status {response.status_code}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error procesando remesa para pedimento {pedimento.pedimento}: {e}", exc_info=True)
|
logger.error(f"Error procesando remesa para pedimento {pedimento.pedimento}: {e}", exc_info=True)
|
||||||
@@ -339,14 +395,18 @@ def procesar_coves(organizacion_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/coves",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/coves",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
timeout=60
|
||||||
|
)
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
response.raise_for_status()
|
||||||
|
logging.info(f"COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando COVEs para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_acuse_coves(organizacion_id):
|
def procesar_acuse_coves(organizacion_id):
|
||||||
@@ -370,14 +430,18 @@ def procesar_acuse_coves(organizacion_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
timeout=60
|
||||||
|
)
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
response.raise_for_status()
|
||||||
|
logging.info(f"Acuses de COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando acuses de COVEs para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_acuses(organizacion_id):
|
def procesar_acuses(organizacion_id):
|
||||||
@@ -401,14 +465,18 @@ def procesar_acuses(organizacion_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
timeout=60
|
||||||
|
)
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
response.raise_for_status()
|
||||||
|
logging.info(f"Acuses encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando acuses para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_edocs(organizacion_id):
|
def procesar_edocs(organizacion_id):
|
||||||
@@ -432,14 +500,18 @@ def procesar_edocs(organizacion_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
try:
|
||||||
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
|
response = requests.post(
|
||||||
data=json.dumps(payload),
|
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
|
||||||
headers={"Content-Type": "application/json"}
|
data=json.dumps(payload),
|
||||||
)
|
headers={"Content-Type": "application/json"},
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
timeout=60
|
||||||
|
)
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
response.raise_for_status()
|
||||||
|
logging.info(f"E-documents encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error encolando E-documents para pedimento {pedimento.pedimento}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_partidas(organizacion_id):
|
def procesar_partidas(organizacion_id):
|
||||||
@@ -449,27 +521,40 @@ def procesar_partidas(organizacion_id):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
for pedimento in pedimentos:
|
for pedimento in pedimentos:
|
||||||
if pedimento.partidas.filter(descargado=False).exists(): # Tipo 4: Partidas
|
partidas_pendientes = list(pedimento.partidas.filter(descargado=False))
|
||||||
# Convertir el pedimento a JSON usando el serializer
|
if not partidas_pendientes:
|
||||||
pedimento_dict = pedimento_to_dict(pedimento)
|
continue
|
||||||
credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first()
|
|
||||||
|
|
||||||
credenciales_dict = credenciales_to_dict(credenciales)
|
pedimento_dict = pedimento_to_dict(pedimento)
|
||||||
|
credenciales = Vucem.objects.filter(
|
||||||
|
id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id
|
||||||
|
).first()
|
||||||
|
credenciales_dict = credenciales_to_dict(credenciales)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"partidas": [partida_to_dict(partida) for partida in pedimento.partidas.filter(descargado=False)],
|
"partidas": [partida_to_dict(p) for p in partidas_pendientes],
|
||||||
"pedimento": pedimento_dict,
|
"pedimento": pedimento_dict,
|
||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{SERVICE_API_URL_V2}/services/all/partidas/",
|
f"{SERVICE_API_URL_V2}/services/all/partidas/",
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
headers={"Content-Type": "application/json"}
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=60
|
||||||
)
|
)
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
logging.info(
|
||||||
|
f"Partidas encoladas para pedimento {pedimento.pedimento}: "
|
||||||
|
f"{result.get('total', 0)} de {len(partidas_pendientes)}"
|
||||||
|
)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(
|
||||||
|
f"Error encolando partidas para pedimento {pedimento.pedimento}: {e}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def documentos_con_errores(organizacion_id):
|
def documentos_con_errores(organizacion_id):
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from .views_auditor import (
|
|||||||
auditor_obtener_peticion_edocument_vu,
|
auditor_obtener_peticion_edocument_vu,
|
||||||
auditor_obtener_respuesta_edocument_vu,
|
auditor_obtener_respuesta_edocument_vu,
|
||||||
auditar_pedimento_endpoint,
|
auditar_pedimento_endpoint,
|
||||||
|
procesar_pedimento_completo_endpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -80,6 +81,7 @@ urlpatterns = [
|
|||||||
path('auditor/auditar-acuse/pedimento/', auditar_acuse_pedimento_endpoint, name='auditar-acuse-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-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-remesa-pedimento'),
|
||||||
path('auditor/auditar-pedimento/', auditar_pedimento_endpoint, name='auditar-pedimento'),
|
path('auditor/auditar-pedimento/', auditar_pedimento_endpoint, name='auditar-pedimento'),
|
||||||
|
path('auditor/procesar-pedimento-completo/pedimento/', procesar_pedimento_completo_endpoint, name='procesar-pedimento-completo-pedimento'),
|
||||||
|
|
||||||
path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'),
|
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/peticion-respuesta/pedimento-vu/', auditar_peticion_respuesta_pedimento_completo, name='peticion-respuesta-pedimento-vu'),
|
||||||
|
|||||||
@@ -10,12 +10,20 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from django.http import HttpResponse
|
||||||
|
import django_filters
|
||||||
|
import io
|
||||||
|
import openpyxl
|
||||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||||
from core.permissions import (
|
from core.permissions import (
|
||||||
IsSameOrganization,
|
IsSameOrganization,
|
||||||
IsSameOrganizationDeveloper,
|
IsSameOrganizationDeveloper,
|
||||||
IsSameOrganizationAndAdmin,
|
IsSameOrganizationAndAdmin,
|
||||||
IsSuperUser
|
IsSuperUser,
|
||||||
|
get_org_context,
|
||||||
|
require_permission,
|
||||||
|
user_has_permission,
|
||||||
|
is_internal_service_request,
|
||||||
)
|
)
|
||||||
from api.customs.models import (
|
from api.customs.models import (
|
||||||
Pedimento,
|
Pedimento,
|
||||||
@@ -244,6 +252,19 @@ class PedimentoPagination(PageNumberPagination):
|
|||||||
return super().paginate_queryset(queryset, request, view)
|
return super().paginate_queryset(queryset, request, view)
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
|
class PedimentoFilter(django_filters.FilterSet):
|
||||||
|
# Rango de fecha de pago: ?fecha_pago_desde=YYYY-MM-DD&fecha_pago_hasta=YYYY-MM-DD
|
||||||
|
fecha_pago_desde = django_filters.DateFilter(field_name='fecha_pago', lookup_expr='gte')
|
||||||
|
fecha_pago_hasta = django_filters.DateFilter(field_name='fecha_pago', lookup_expr='lte')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Pedimento
|
||||||
|
fields = [
|
||||||
|
'patente', 'aduana', 'tipo_operacion', 'clave_pedimento',
|
||||||
|
'pedimento', 'existe_expediente', 'contribuyente',
|
||||||
|
'curp_apoderado', 'fecha_pago', 'pedimento_app',
|
||||||
|
]
|
||||||
class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): # Pendiente de permisos de creacion
|
class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): # Pendiente de permisos de creacion
|
||||||
"""
|
"""
|
||||||
ViewSet for Pedimento model.
|
ViewSet for Pedimento model.
|
||||||
@@ -257,7 +278,9 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
- existe_expediente: Filtro por expediente (True/False)
|
- existe_expediente: Filtro por expediente (True/False)
|
||||||
- contribuyente: Filtro por contribuyente
|
- contribuyente: Filtro por contribuyente
|
||||||
- curp_apoderado: Filtro por curp del apoderado
|
- curp_apoderado: Filtro por curp del apoderado
|
||||||
- fecha_pago: Filtro por fecha de pago (YYYY-MM-DD)
|
- fecha_pago: Filtro por fecha de pago exacta (YYYY-MM-DD)
|
||||||
|
- fecha_pago_desde: Rango inicio de fecha de pago (YYYY-MM-DD)
|
||||||
|
- fecha_pago_hasta: Rango fin de fecha de pago (YYYY-MM-DD)
|
||||||
- patente: Filtro por patente
|
- patente: Filtro por patente
|
||||||
- aduana: Filtro por aduana
|
- aduana: Filtro por aduana
|
||||||
- tipo_operacion: Filtro por tipo de operación
|
- tipo_operacion: Filtro por tipo de operación
|
||||||
@@ -267,43 +290,112 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
Ejemplos:
|
Ejemplos:
|
||||||
- /pedimentos/ → Devuelve TODOS los pedimentos
|
- /pedimentos/ → Devuelve TODOS los pedimentos
|
||||||
- /pedimentos/?page_size=10 → Devuelve los primeros 10
|
- /pedimentos/?page_size=10 → Devuelve los primeros 10
|
||||||
- /pedimentos/?page_size=10&page=2 → Devuelve los pedimentos 11-20
|
- /pedimentos/?fecha_pago_desde=2025-01-01&fecha_pago_hasta=2025-12-31 → Rango de fechas
|
||||||
- /pedimentos/?pedimento=12345678 → Filtra por número de pedimento
|
- /pedimentos/export-excel/?contribuyente=EMPRESA → Descarga Excel con filtros
|
||||||
- /pedimentos/?existe_expediente=true → Filtra por expediente existente
|
|
||||||
- /pedimentos/?contribuyente=EMPRESA → Filtra por contribuyente
|
|
||||||
- /pedimentos/?curp_apoderado=XXXX → Filtra por curp apoderado
|
|
||||||
- /pedimentos/?fecha_pago=2025-07-18 → Filtra por fecha de pago
|
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
serializer_class = PedimentoSerializer
|
serializer_class = PedimentoSerializer
|
||||||
pagination_class = PedimentoPagination
|
pagination_class = PedimentoPagination
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
model = Pedimento
|
model = Pedimento
|
||||||
|
|
||||||
filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app']
|
filterset_class = PedimentoFilter
|
||||||
search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento']
|
search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento']
|
||||||
# AGREGAR ESTOS CAMPOS PARA ORDENACIÓN
|
|
||||||
ordering_fields = ['created_at', 'pedimento', 'fecha_pago', 'aduana', 'patente']
|
ordering_fields = ['created_at', 'pedimento', 'fecha_pago', 'aduana', 'patente']
|
||||||
ordering = ['-created_at'] # Orden descendente por fecha de creación por defecto
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
perms = {
|
||||||
|
'list': 'pedimentos.view',
|
||||||
|
'retrieve': 'pedimentos.view',
|
||||||
|
'create': 'pedimentos.create',
|
||||||
|
'update': 'pedimentos.edit',
|
||||||
|
'partial_update': 'pedimentos.edit',
|
||||||
|
'destroy': 'pedimentos.delete',
|
||||||
|
'procesar_completo': 'pedimentos.process',
|
||||||
|
'procesar_partidas': 'pedimentos.process',
|
||||||
|
'procesar_coves': 'pedimentos.process',
|
||||||
|
'procesar_acuse_coves': 'pedimentos.process',
|
||||||
|
'procesar_edocs': 'pedimentos.process',
|
||||||
|
'procesar_acuses': 'pedimentos.process',
|
||||||
|
'procesar_remesas': 'pedimentos.process',
|
||||||
|
'bulk_delete': 'pedimentos.delete',
|
||||||
|
'bulk_create': 'pedimentos.create',
|
||||||
|
'bulk_create_pedimento_desk': 'pedimentos.create',
|
||||||
|
'bulk_upload_record': 'documentos.upload',
|
||||||
|
'bulk_upload_record_async': 'documentos.upload',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'pedimentos.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if not user_has_permission(self.request.user, 'pedimentos.view'):
|
||||||
|
return Pedimento.objects.none()
|
||||||
|
return self.get_queryset_filtrado_por_organizacion()
|
||||||
|
|
||||||
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
|
@action(detail=False, methods=['get'], url_path='export-excel')
|
||||||
|
def export_excel(self, request):
|
||||||
|
"""Exporta a Excel todos los pedimentos que coincidan con los filtros activos."""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
# pedimento_app_filter = self.request.GET.get('pedimento_app', None)
|
columnas = [
|
||||||
|
('pedimento_app', 'Pedimento'),
|
||||||
|
('fecha_pago', 'Fecha Pago'),
|
||||||
|
('aduana', 'Aduana'),
|
||||||
|
('patente', 'Patente'),
|
||||||
|
('contribuyente', 'Contribuyente'),
|
||||||
|
('curp_apoderado','CURP Apoderado'),
|
||||||
|
('numero_partidas','Partidas'),
|
||||||
|
('created_at', 'F. Carga'),
|
||||||
|
('tipo_operacion','Tipo Op.'),
|
||||||
|
('clave_pedimento','Clave Pedimento'),
|
||||||
|
('documentos_count', 'Archivos'),
|
||||||
|
('existe_expediente','Expediente'),
|
||||||
|
]
|
||||||
|
|
||||||
# if pedimento_app_filter:
|
def safe_value(val):
|
||||||
# print(f"Filtro por pedimento_app: {pedimento_app_filter}")
|
if val is None:
|
||||||
# queryset = queryset.filter(pedimento_app__icontains=pedimento_app_filter)
|
return ''
|
||||||
|
if isinstance(val, bool):
|
||||||
|
return 'Sí' if val else 'No'
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
return val
|
||||||
|
if isinstance(val, (datetime, date)):
|
||||||
|
return str(val)[:10]
|
||||||
|
# ForeignKey instances u otros objetos Django → su representación string
|
||||||
|
return str(val)
|
||||||
|
|
||||||
return queryset
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = 'Pedimentos'
|
||||||
|
|
||||||
|
ws.append([label for _, label in columnas])
|
||||||
|
|
||||||
|
for ped in queryset.iterator():
|
||||||
|
fila = []
|
||||||
|
for campo, _ in columnas:
|
||||||
|
val = getattr(ped, campo, None)
|
||||||
|
fila.append(safe_value(val))
|
||||||
|
ws.append(fila)
|
||||||
|
|
||||||
|
# Autoajuste de ancho de columnas
|
||||||
|
for col in ws.columns:
|
||||||
|
max_len = max((len(str(cell.value or '')) for cell in col), default=10)
|
||||||
|
ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 50)
|
||||||
|
|
||||||
|
output = io.BytesIO()
|
||||||
|
wb.save(output)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
filename = f"pedimentos_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
response = HttpResponse(
|
||||||
|
output.read(),
|
||||||
|
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
)
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||||
|
return response
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""
|
org = get_org_context(self.request.user)
|
||||||
Asigna automáticamente la organización del usuario autenticado al crear un pedimento.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
|
||||||
raise ValueError("Usuario no autenticado o sin organización")
|
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
if not data.get('pedimento_app'):
|
if not data.get('pedimento_app'):
|
||||||
fecha_pago = data.get('fecha_pago')
|
fecha_pago = data.get('fecha_pago')
|
||||||
@@ -312,7 +404,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
pedimento = data.get('pedimento')
|
pedimento = data.get('pedimento')
|
||||||
if fecha_pago and aduana and patente and pedimento:
|
if fecha_pago and aduana and patente and pedimento:
|
||||||
pedimento_app = f"{str(fecha_pago.year)[-2:]}-{str(aduana).zfill(2)[-2:]}-{str(patente).zfill(4)[-4:]}-{str(pedimento).zfill(7)[-7:]}"
|
pedimento_app = f"{str(fecha_pago.year)[-2:]}-{str(aduana).zfill(2)[-2:]}-{str(patente).zfill(4)[-4:]}-{str(pedimento).zfill(7)[-7:]}"
|
||||||
serializer.save(organizacion=self.request.user.organizacion, pedimento_app=pedimento_app)
|
serializer.save(organizacion=org, pedimento_app=pedimento_app)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Usar el nombre del servicio de Docker Compose en lugar de localhost
|
# Usar el nombre del servicio de Docker Compose en lugar de localhost
|
||||||
@@ -375,6 +467,9 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='procesar-completo')
|
@action(detail=True, methods=['post'], url_path='procesar-completo')
|
||||||
def procesar_completo(self, request, pk=None):
|
def procesar_completo(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
@@ -2200,30 +2295,67 @@ class PartidaViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
Ejemplo: GET /api/partidas/?pedimento=6782d22e-5e97-4efc-87c9-bd8497c8ac7e
|
Ejemplo: GET /api/partidas/?pedimento=6782d22e-5e97-4efc-87c9-bd8497c8ac7e
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
queryset = Partida.objects.all()
|
|
||||||
serializer_class = PartidaSerializer
|
serializer_class = PartidaSerializer
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
'pedimento': ['exact'], # Filtro directo por UUID del pedimento
|
'pedimento': ['exact'],
|
||||||
'pedimento__id': ['exact'], # Filtro alternativo
|
'pedimento__id': ['exact'],
|
||||||
'numero_partida': ['exact', 'gte', 'lte'], # Filtros por número de partida
|
'numero_partida': ['exact', 'gte', 'lte'],
|
||||||
'descargado': ['exact'], # Filtro por estado de descarga
|
'descargado': ['exact'],
|
||||||
'created_at': ['exact', 'gte', 'lte'], # Filtros por fecha de creación
|
'created_at': ['exact', 'gte', 'lte'],
|
||||||
'updated_at': ['exact', 'gte', 'lte'] # Filtros por fecha de actualización
|
'updated_at': ['exact', 'gte', 'lte'],
|
||||||
}
|
}
|
||||||
search_fields = ['pedimento__pedimento', 'pedimento__pedimento_app']
|
search_fields = ['pedimento__pedimento', 'pedimento__pedimento_app']
|
||||||
ordering_fields = ['numero_partida', 'pedimento__pedimento', 'id', 'created_at', 'updated_at']
|
ordering_fields = ['numero_partida', 'pedimento__pedimento', 'id', 'created_at', 'updated_at']
|
||||||
ordering = ['numero_partida'] # Ordenar por número de partida por defecto
|
ordering = ['numero_partida']
|
||||||
|
|
||||||
my_tags = ['Partidas']
|
my_tags = ['Partidas']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
perms = {
|
||||||
|
'list': 'partidas.view',
|
||||||
|
'retrieve': 'partidas.view',
|
||||||
|
'create': 'partidas.create',
|
||||||
|
'update': 'partidas.edit',
|
||||||
|
'partial_update': 'partidas.edit',
|
||||||
|
'destroy': 'partidas.delete',
|
||||||
|
'bulk_delete_partidas_vu': 'partidas.delete',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'partidas.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
return Partida.objects.all()
|
||||||
|
if not user_has_permission(user, 'partidas.view'):
|
||||||
|
return Partida.objects.none()
|
||||||
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return Partida.objects.none()
|
||||||
|
qs = Partida.objects.filter(pedimento__organizacion=org)
|
||||||
|
if user.is_importador:
|
||||||
|
qs = qs.filter(pedimento__contribuyente__in=user.rfc.all())
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
serializer.save()
|
||||||
|
return
|
||||||
|
pedimento = serializer.validated_data.get('pedimento')
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
if pedimento and pedimento.organizacion != org:
|
||||||
|
raise PermissionDenied("El pedimento no pertenece a tu organización.")
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
|
class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
ViewSet for TipoOperacion model.
|
ViewSet for TipoOperacion model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('pedimentos.view')]
|
||||||
|
|
||||||
queryset = TipoOperacion.objects.all()
|
queryset = TipoOperacion.objects.all()
|
||||||
serializer_class = TipoOperacionSerializer
|
serializer_class = TipoOperacionSerializer
|
||||||
@@ -2236,6 +2368,14 @@ class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
|
|||||||
|
|
||||||
my_tags = ['Tipos_Operacion']
|
my_tags = ['Tipos_Operacion']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
return TipoOperacion.objects.all()
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
return TipoOperacion.objects.none()
|
||||||
|
return TipoOperacion.objects.filter(organizacion=org)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""
|
"""
|
||||||
Asigna automáticamente la organización del usuario autenticado al crear un tipo de operación.
|
Asigna automáticamente la organización del usuario autenticado al crear un tipo de operación.
|
||||||
@@ -2276,7 +2416,6 @@ class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizaci
|
|||||||
- /procesamientopedimentos/ → Devuelve TODOS los procesamientos
|
- /procesamientopedimentos/ → Devuelve TODOS los procesamientos
|
||||||
- /procesamientopedimentos/?page_size=5 → Devuelve los primeros 5
|
- /procesamientopedimentos/?page_size=5 → Devuelve los primeros 5
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated, IsSuperUser | IsSameOrganizationDeveloper ]
|
|
||||||
serializer_class = ProcesamientoPedimentoSerializer
|
serializer_class = ProcesamientoPedimentoSerializer
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
model = ProcesamientoPedimento
|
model = ProcesamientoPedimento
|
||||||
@@ -2292,51 +2431,53 @@ class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizaci
|
|||||||
ordering_fields = ['created_at', 'updated_at']
|
ordering_fields = ['created_at', 'updated_at']
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
perms = {
|
||||||
|
'list': 'pedimentos.view',
|
||||||
|
'retrieve': 'pedimentos.view',
|
||||||
|
'create': 'pedimentos.process',
|
||||||
|
'update': 'pedimentos.process',
|
||||||
|
'partial_update': 'pedimentos.process',
|
||||||
|
'destroy': 'pedimentos.process',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'pedimentos.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.get_queryset_filtrado_por_organizacion()
|
user = self.request.user
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
return ProcesamientoPedimento.objects.all()
|
||||||
|
if not user_has_permission(user, 'pedimentos.view'):
|
||||||
|
return ProcesamientoPedimento.objects.none()
|
||||||
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return ProcesamientoPedimento.objects.none()
|
||||||
|
if user.is_importador:
|
||||||
|
return ProcesamientoPedimento.objects.filter(
|
||||||
|
organizacion=org,
|
||||||
|
pedimento__contribuyente__in=user.rfc.all()
|
||||||
|
)
|
||||||
|
return ProcesamientoPedimento.objects.filter(organizacion=org)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Asigna siempre la organización al crear un procesamiento de pedimento.
|
|
||||||
- Para superusuarios: requiere que la organización venga explícitamente en los datos validados.
|
|
||||||
- Para usuarios normales: asigna la organización del usuario autenticado.
|
|
||||||
"""
|
|
||||||
user = self.request.user
|
|
||||||
if not user.is_authenticated:
|
|
||||||
raise ValueError("Usuario no autenticado")
|
|
||||||
|
|
||||||
# Si es superusuario, debe venir la organización en los datos validados
|
|
||||||
if user.is_superuser:
|
|
||||||
organizacion = serializer.validated_data.get('organizacion', None)
|
|
||||||
if not organizacion:
|
|
||||||
raise ValueError("El superusuario debe especificar una organización al crear el procesamiento de pedimento.")
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
# Para usuarios normales, asignar siempre la organización del usuario
|
if not org:
|
||||||
if not hasattr(user, 'organizacion') or not user.organizacion:
|
raise PermissionDenied("Sin organización activa.")
|
||||||
raise ValueError("Usuario sin organización")
|
serializer.save(organizacion=org)
|
||||||
serializer.save(organizacion=user.organizacion)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Permite actualizar un procesamiento de pedimento, pero solo si el usuario es superusuario o pertenece a la misma organización.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
raise ValueError("Usuario no autenticado")
|
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
if not user_has_permission(self.request.user, 'pedimentos.process'):
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
raise PermissionDenied("Se requiere el permiso pedimentos.process.")
|
||||||
# Para usuarios normales, usar siempre su organización
|
org = get_org_context(self.request.user)
|
||||||
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
if not org:
|
||||||
raise ValueError("Usuario sin organización")
|
raise PermissionDenied("Sin organización activa.")
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
serializer.save(organizacion=org)
|
||||||
return
|
|
||||||
|
|
||||||
raise ValueError("Usuario no autenticado o sin permisos para actualizar ProcesamientoPedimento")
|
|
||||||
|
|
||||||
my_tags = ['Procesamientos_Pedimentos']
|
my_tags = ['Procesamientos_Pedimentos']
|
||||||
|
|
||||||
@@ -2344,7 +2485,6 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
"""
|
"""
|
||||||
ViewSet for EDocument model.
|
ViewSet for EDocument model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
serializer_class = EDocumentSerializer
|
serializer_class = EDocumentSerializer
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
@@ -2353,60 +2493,131 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
ordering_fields = ['created_at', 'updated_at', 'numero_edocument']
|
ordering_fields = ['created_at', 'updated_at', 'numero_edocument']
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
model = EDocument
|
model = EDocument
|
||||||
|
campo_contribuyente = 'pedimento__contribuyente'
|
||||||
my_tags = ['EDocuments']
|
my_tags = ['EDocuments']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
perms = {
|
||||||
|
'list': 'edocuments.view',
|
||||||
|
'retrieve': 'edocuments.view',
|
||||||
|
'create': 'edocuments.create',
|
||||||
|
'update': 'edocuments.edit',
|
||||||
|
'partial_update': 'edocuments.edit',
|
||||||
|
'destroy': 'edocuments.delete',
|
||||||
|
'bulk_delete_edocs_vu': 'edocuments.delete',
|
||||||
|
'reset_acuse': 'edocuments.edit',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'edocuments.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if not user_has_permission(self.request.user, 'edocuments.view'):
|
||||||
|
return EDocument.objects.none()
|
||||||
return self.get_queryset_filtrado_por_organizacion()
|
return self.get_queryset_filtrado_por_organizacion()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Asigna automáticamente la organización del usuario autenticado al crear un EDocument.
|
|
||||||
Para superusuarios, permite especificar una organización diferente.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
raise ValueError("Usuario no autenticado")
|
|
||||||
|
|
||||||
# Si es superusuario y se especifica organizacion en los datos validados
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
# Permitir que el superusuario especifique la organización
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
print(f"self.request.user.groups >>>> {self.request.user.groups}")
|
serializer.save(organizacion=org)
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
|
||||||
# Para usuarios normales, usar siempre su organización
|
|
||||||
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
|
||||||
raise ValueError("Usuario sin organización")
|
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ValueError("Usuario no autenticado o sin permisos para crear EDocument")
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Permite actualizar un EDocument, pero solo si el usuario es superusuario o pertenece a la misma organización.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
raise ValueError("Usuario no autenticado")
|
|
||||||
|
|
||||||
# Si es superusuario, permite actualizar sin restricciones
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
serializer.save(organizacion=org)
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
def perform_destroy(self, instance):
|
||||||
# Para usuarios normales, usar siempre su organización
|
instance.delete()
|
||||||
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
|
||||||
raise ValueError("Usuario sin organización")
|
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
|
|
||||||
raise ValueError("Usuario no autenticado o sin permisos para actualizar EDocument")
|
@action(detail=True, methods=['post'], url_path='reset-acuse')
|
||||||
|
def reset_acuse(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Detecta inconsistencia cuando acuse_descargado=True pero no existe el documento
|
||||||
|
de acuse (tipo 4). Crea un registro de error tipo 26 para Errores VU y
|
||||||
|
restablece acuse_descargado=False para permitir reintentar.
|
||||||
|
"""
|
||||||
|
from api.record.models import Document, DocumentType
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('api.customs.views')
|
||||||
|
|
||||||
|
edoc = self.get_object()
|
||||||
|
|
||||||
|
if not edoc.acuse_descargado:
|
||||||
|
return Response(
|
||||||
|
{"error": "El acuse no está marcado como descargado"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verificar si el acuse PDF (tipo 4 = Pedimento Acuse) existe realmente
|
||||||
|
acuse_disponible = Document.objects.filter(
|
||||||
|
pedimento=edoc.pedimento,
|
||||||
|
archivo__icontains=edoc.numero_edocument,
|
||||||
|
document_type_id=4
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
if acuse_disponible:
|
||||||
|
return Response(
|
||||||
|
{"status": "El acuse está disponible correctamente", "acuse_disponible": True},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inconsistencia confirmada: crear documento de error tipo 26 para Errores VU
|
||||||
|
doc_type_error = DocumentType.objects.filter(id=26).first()
|
||||||
|
if doc_type_error:
|
||||||
|
error_content = (
|
||||||
|
f"Inconsistencia detectada: el acuse del EDocument {edoc.numero_edocument} "
|
||||||
|
f"fue marcado como descargado pero el documento no se encuentra disponible. "
|
||||||
|
f"El estado fue restablecido para permitir reprocesamiento."
|
||||||
|
).encode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='wb', suffix='.txt', delete=False
|
||||||
|
) as f:
|
||||||
|
f.write(error_content)
|
||||||
|
tmp_path = f.name
|
||||||
|
|
||||||
|
pedimento_app = getattr(edoc.pedimento, 'pedimento_app', str(edoc.pedimento.pedimento))
|
||||||
|
file_name = f"error_acuse_{edoc.numero_edocument}.txt"
|
||||||
|
|
||||||
|
saved_path = storage_service.save_document_from_path(
|
||||||
|
file_path=tmp_path,
|
||||||
|
file_name=file_name,
|
||||||
|
organizacion_id=edoc.organizacion_id,
|
||||||
|
pedimento_app=pedimento_app
|
||||||
|
)
|
||||||
|
|
||||||
|
if saved_path:
|
||||||
|
Document.objects.create(
|
||||||
|
organizacion=edoc.organizacion,
|
||||||
|
pedimento=edoc.pedimento,
|
||||||
|
archivo=saved_path,
|
||||||
|
document_type=doc_type_error,
|
||||||
|
extension='TXT',
|
||||||
|
size=len(error_content),
|
||||||
|
fuente=None,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error creando documento de error para acuse {edoc.numero_edocument}: {e}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
edoc.acuse_descargado = False
|
||||||
|
edoc.save()
|
||||||
|
|
||||||
|
serializer = self.get_serializer(edoc)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||||
"""
|
"""
|
||||||
ViewSet for Cove model.
|
ViewSet for Cove model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSuperUser |IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
|
|
||||||
serializer_class = CoveSerializer
|
serializer_class = CoveSerializer
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
@@ -2415,61 +2626,48 @@ class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
|||||||
ordering_fields = ['created_at', 'updated_at', 'numero_cove']
|
ordering_fields = ['created_at', 'updated_at', 'numero_cove']
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
model = Cove
|
model = Cove
|
||||||
|
campo_contribuyente = 'pedimento__contribuyente'
|
||||||
my_tags = ['Coves']
|
my_tags = ['Coves']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
perms = {
|
||||||
|
'list': 'coves.view',
|
||||||
|
'retrieve': 'coves.view',
|
||||||
|
'create': 'coves.create',
|
||||||
|
'update': 'coves.edit',
|
||||||
|
'partial_update': 'coves.edit',
|
||||||
|
'destroy': 'coves.delete',
|
||||||
|
'bulk_delete_coves_vu': 'coves.delete',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'coves.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if not user_has_permission(self.request.user, 'coves.view'):
|
||||||
|
return Cove.objects.none()
|
||||||
return self.get_queryset_filtrado_por_organizacion()
|
return self.get_queryset_filtrado_por_organizacion()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Asigna automáticamente la organización del usuario autenticado al crear un Cove.
|
|
||||||
Para superusuarios, permite especificar una organización diferente.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
raise ValueError("Usuario no autenticado")
|
|
||||||
|
|
||||||
# Si es superusuario y se especifica organizacion en los datos validados
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
# Permitir que el superusuario especifique la organización
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
if (
|
serializer.save(organizacion=org)
|
||||||
self.request.user.groups.filter(name='developer').exists()
|
|
||||||
or self.request.user.groups.filter(name='admin').exists()
|
|
||||||
or self.request.user.groups.filter(name='user').exists()
|
|
||||||
) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
|
||||||
# Para usuarios normales, usar siempre su organización
|
|
||||||
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
|
||||||
raise ValueError("Usuario sin organización")
|
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ValueError("Usuario no autenticado o sin permisos para crear Cove")
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Permite actualizar un Cove, pero solo si el usuario es superusuario o pertenece a la misma organización.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
raise ValueError("Usuario no autenticado")
|
|
||||||
|
|
||||||
# Si es superusuario, permite actualizar sin restricciones
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
serializer.save(organizacion=org)
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user .groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
def perform_destroy(self, instance):
|
||||||
# Para usuarios normales, usar siempre su organización
|
instance.delete()
|
||||||
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
|
||||||
raise ValueError("Usuario sin organización")
|
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
|
|
||||||
class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
class ImportadorViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
ViewSet for Importador model.
|
ViewSet for Importador model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
serializer_class = ImportadorSerializer
|
serializer_class = ImportadorSerializer
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
@@ -2477,69 +2675,69 @@ class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
|||||||
search_fields = ['rfc', 'nombre']
|
search_fields = ['rfc', 'nombre']
|
||||||
ordering_fields = ['created_at', 'updated_at', 'rfc']
|
ordering_fields = ['created_at', 'updated_at', 'rfc']
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
model = Importador
|
my_tags = ['Importadores']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
# list/retrieve: solo IsAuthenticated — el queryset filtra según permisos
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
return [IsAuthenticated()]
|
||||||
|
perms = {
|
||||||
|
'create': 'importadores.create',
|
||||||
|
'update': 'importadores.edit',
|
||||||
|
'partial_update': 'importadores.edit',
|
||||||
|
'destroy': 'importadores.delete',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'importadores.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
grupos = user.groups.values_list('name', flat=True)
|
if is_internal_service_request(self.request):
|
||||||
|
|
||||||
if user.is_superuser:
|
|
||||||
return Importador.objects.all()
|
return Importador.objects.all()
|
||||||
|
org = get_org_context(user)
|
||||||
if 'Importador' in grupos:
|
if not org:
|
||||||
return user.rfc.all()
|
return Importador.objects.none()
|
||||||
|
# Con permiso ve todos; sin permiso solo los asignados al usuario
|
||||||
return self.get_queryset_filtrado_por_organizacion()
|
if user_has_permission(user, 'importadores.view'):
|
||||||
|
return Importador.objects.filter(organizacion=org)
|
||||||
|
return Importador.objects.filter(organizacion=org, users=user)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if is_internal_service_request(self.request):
|
||||||
raise ValueError("Usuario no autenticado o sin organización")
|
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
|
||||||
raise ValueError("Usuario no autenticado o sin organización")
|
|
||||||
|
|
||||||
# Si es superusuario, permite actualizar sin restricciones
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
serializer.save(organizacion=org)
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
def perform_update(self, serializer):
|
||||||
# Para usuarios normales, usar siempre su organización
|
if is_internal_service_request(self.request):
|
||||||
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
serializer.save()
|
||||||
raise ValueError("Usuario sin organización")
|
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
serializer.save(organizacion=org)
|
||||||
|
|
||||||
raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador")
|
def perform_destroy(self, instance):
|
||||||
|
instance.delete()
|
||||||
my_tags = ['Importadores']
|
|
||||||
|
|
||||||
class EjecutarComandoView(APIView):
|
class EjecutarComandoView(APIView):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
"""
|
"""
|
||||||
View para ejecutar el comando de microservicios desde una petición HTTP.
|
View para ejecutar el comando de microservicios desde una petición HTTP.
|
||||||
"""
|
"""
|
||||||
def post(self, request):
|
permission_classes = [IsAuthenticated, require_permission('pedimentos.process')]
|
||||||
|
|
||||||
# Obtener organizacion_id del request (si se envía)
|
def post(self, request):
|
||||||
organizacion_id_request = request.data.get('organizacionid', None)
|
|
||||||
procesamiento = request.data.get('procesamiento', None)
|
procesamiento = request.data.get('procesamiento', None)
|
||||||
todos = request.data.get('todos', False)
|
todos = request.data.get('todos', False)
|
||||||
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
org = get_org_context(request.user)
|
||||||
raise ValueError("Usuario no autenticado o sin organización")
|
if not org:
|
||||||
|
|
||||||
if organizacion_id_request is None:
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": 'No se proporcionó la organización a ejecutar el proceso.'},
|
{"error": "Sin organización activa."},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
# organizacion_id = self.request.user.organizacion.id
|
organizacion_id = str(org.id)
|
||||||
organizacion_id = organizacion_id_request
|
nombre_organizacion = org.nombre
|
||||||
nombre_organizacion = self.request.user.organizacion.nombre
|
|
||||||
|
|
||||||
if procesamiento is None and todos == False:
|
if procesamiento is None and todos == False:
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from core.permissions import IsSuperUser, IsSameOrganizationDeveloper
|
from core.permissions import require_permission
|
||||||
from .tasks.auditoria import (
|
from .tasks.auditoria import (
|
||||||
crear_partidas,
|
crear_partidas,
|
||||||
auditar_coves,
|
auditar_coves,
|
||||||
@@ -15,7 +15,7 @@ from .tasks.auditoria import (
|
|||||||
auditar_remesas,
|
auditar_remesas,
|
||||||
)
|
)
|
||||||
from .tasks.internal_services import auditar_pedimentos
|
from .tasks.internal_services import auditar_pedimentos
|
||||||
from .tasks.microservice_v2 import procesar_pedimentos_completos
|
from .tasks.microservice_v2 import procesar_pedimentos_completos, procesar_pedimento_completo_individual
|
||||||
from api.customs.models import Pedimento
|
from api.customs.models import Pedimento
|
||||||
from api.organization.models import Organizacion
|
from api.organization.models import Organizacion
|
||||||
from api.record.models import Document
|
from api.record.models import Document
|
||||||
@@ -25,6 +25,9 @@ import os
|
|||||||
from api.utils.storage_service import storage_service
|
from api.utils.storage_service import storage_service
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
_ERROR_DOCUMENT_TYPES = [10, 14, 16, 18, 20, 22, 24, 26]
|
||||||
|
|
||||||
logger = logging.getLogger('api.customs.views_auditor')
|
logger = logging.getLogger('api.customs.views_auditor')
|
||||||
|
|
||||||
def get_document_content(documento):
|
def get_document_content(documento):
|
||||||
@@ -84,7 +87,7 @@ def get_document_path(documento):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def crear_partidas_organizacion(request):
|
def crear_partidas_organizacion(request):
|
||||||
organizacion_id = request.data.get('organizacion_id')
|
organizacion_id = request.data.get('organizacion_id')
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ def crear_partidas_organizacion(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def crear_partidas_pedimento(request):
|
def crear_partidas_pedimento(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ def crear_partidas_pedimento(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_pedimentos_endpoint(request):
|
def auditar_pedimentos_endpoint(request):
|
||||||
"""
|
"""
|
||||||
Inicia una tarea de auditoría para todos los pedimentos de una organización.
|
Inicia una tarea de auditoría para todos los pedimentos de una organización.
|
||||||
@@ -252,7 +255,7 @@ def auditar_pedimentos_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditar_procesamiento_remesa_pedimento_endpoint(request):
|
def auditar_procesamiento_remesa_pedimento_endpoint(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
|
|
||||||
@@ -339,7 +342,7 @@ def _lanzar_auditoria_organizacion(request, task_fn, label):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_coves_endpoint(request):
|
def auditar_coves_endpoint(request):
|
||||||
return _lanzar_auditoria_organizacion(request, auditar_coves, 'COVEs')
|
return _lanzar_auditoria_organizacion(request, auditar_coves, 'COVEs')
|
||||||
|
|
||||||
@@ -359,7 +362,7 @@ def auditar_coves_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_acuse_cove_endpoint(request):
|
def auditar_acuse_cove_endpoint(request):
|
||||||
return _lanzar_auditoria_organizacion(request, auditar_acuse_cove, 'acuses de COVE')
|
return _lanzar_auditoria_organizacion(request, auditar_acuse_cove, 'acuses de COVE')
|
||||||
|
|
||||||
@@ -379,7 +382,7 @@ def auditar_acuse_cove_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_edocuments_endpoint(request):
|
def auditar_edocuments_endpoint(request):
|
||||||
return _lanzar_auditoria_organizacion(request, auditar_edocuments, 'EDocuments')
|
return _lanzar_auditoria_organizacion(request, auditar_edocuments, 'EDocuments')
|
||||||
|
|
||||||
@@ -399,7 +402,7 @@ def auditar_edocuments_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_acuse_endpoint(request):
|
def auditar_acuse_endpoint(request):
|
||||||
return _lanzar_auditoria_organizacion(request, auditar_acuse, 'acuses de EDocument')
|
return _lanzar_auditoria_organizacion(request, auditar_acuse, 'acuses de EDocument')
|
||||||
|
|
||||||
@@ -419,7 +422,7 @@ def auditar_acuse_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_remesas_endpoint(request):
|
def auditar_remesas_endpoint(request):
|
||||||
return _lanzar_auditoria_organizacion(request, auditar_remesas, 'remesas')
|
return _lanzar_auditoria_organizacion(request, auditar_remesas, 'remesas')
|
||||||
|
|
||||||
@@ -442,7 +445,7 @@ def auditar_remesas_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditar_cove_pedimento_endpoint(request):
|
def auditar_cove_pedimento_endpoint(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
@@ -504,7 +507,7 @@ def auditar_cove_pedimento_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditar_acuse_cove_pedimento_endpoint(request):
|
def auditar_acuse_cove_pedimento_endpoint(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
@@ -566,7 +569,7 @@ def auditar_acuse_cove_pedimento_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditar_edocument_pedimento_endpoint(request):
|
def auditar_edocument_pedimento_endpoint(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
@@ -628,7 +631,7 @@ def auditar_edocument_pedimento_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditar_acuse_pedimento_endpoint(request):
|
def auditar_acuse_pedimento_endpoint(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
@@ -687,7 +690,7 @@ def auditar_acuse_pedimento_endpoint(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditor_procesar_pedimentos_organizacion(request):
|
def auditor_procesar_pedimentos_organizacion(request):
|
||||||
"""
|
"""
|
||||||
Inicia una tarea de procesamiento para todos los pedimentos de todas las organizaciones.
|
Inicia una tarea de procesamiento para todos los pedimentos de todas las organizaciones.
|
||||||
@@ -739,7 +742,7 @@ def auditor_procesar_pedimentos_organizacion(request):
|
|||||||
### Fin Procesamiento de pedimentos ###
|
### Fin Procesamiento de pedimentos ###
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditar_peticion_respuesta_pedimento_completo(request):
|
def auditar_peticion_respuesta_pedimento_completo(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento.
|
Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento.
|
||||||
@@ -884,7 +887,7 @@ def auditar_peticion_respuesta_pedimento_completo(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_pedimento_vu(request):
|
def auditor_obtener_peticion_pedimento_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento.
|
Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento.
|
||||||
@@ -938,7 +941,7 @@ def auditor_obtener_peticion_pedimento_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_pedimento_vu(request):
|
def auditor_obtener_respuesta_pedimento_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a un pedimento.
|
Backend endpoint para obtener las respuestas asociadas a un pedimento.
|
||||||
@@ -991,7 +994,7 @@ def auditor_obtener_respuesta_pedimento_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_remesa_vu(request):
|
def auditor_obtener_peticion_remesa_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
||||||
@@ -1045,7 +1048,7 @@ def auditor_obtener_peticion_remesa_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_remesa_vu(request):
|
def auditor_obtener_respuesta_remesa_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
||||||
@@ -1098,7 +1101,7 @@ def auditor_obtener_respuesta_remesa_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_partidas_vu(request):
|
def auditor_obtener_peticion_partidas_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
||||||
@@ -1178,7 +1181,7 @@ def auditor_obtener_peticion_partidas_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_partidas_vu(request):
|
def auditor_obtener_respuesta_partidas_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
||||||
@@ -1231,7 +1234,7 @@ def auditor_obtener_respuesta_partidas_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_acuse_vu(request):
|
def auditor_obtener_peticion_acuse_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
||||||
@@ -1285,7 +1288,7 @@ def auditor_obtener_peticion_acuse_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_acuse_vu(request):
|
def auditor_obtener_respuesta_acuse_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
||||||
@@ -1338,7 +1341,7 @@ def auditor_obtener_respuesta_acuse_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_cove_vu(request):
|
def auditor_obtener_peticion_cove_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
||||||
@@ -1392,7 +1395,7 @@ def auditor_obtener_peticion_cove_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_cove_vu(request):
|
def auditor_obtener_respuesta_cove_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
||||||
@@ -1445,7 +1448,7 @@ def auditor_obtener_respuesta_cove_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_acuse_cove_vu(request):
|
def auditor_obtener_peticion_acuse_cove_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
||||||
@@ -1499,7 +1502,7 @@ def auditor_obtener_peticion_acuse_cove_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_acuse_cove_vu(request):
|
def auditor_obtener_respuesta_acuse_cove_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
||||||
@@ -1552,7 +1555,7 @@ def auditor_obtener_respuesta_acuse_cove_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_peticion_edocument_vu(request):
|
def auditor_obtener_peticion_edocument_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
Backend endpoint para obtener las peticiones asociadas a una remesa.
|
||||||
@@ -1606,7 +1609,7 @@ def auditor_obtener_peticion_edocument_vu(request):
|
|||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
|
||||||
def auditor_obtener_respuesta_edocument_vu(request):
|
def auditor_obtener_respuesta_edocument_vu(request):
|
||||||
"""
|
"""
|
||||||
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
Backend endpoint para obtener las respuestas asociadas a una remesa.
|
||||||
@@ -1677,10 +1680,11 @@ def auditor_obtener_respuesta_edocument_vu(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
def auditar_pedimento_endpoint(request):
|
def auditar_pedimento_endpoint(request):
|
||||||
"""
|
"""
|
||||||
Audita un pedimento específico verificando si existe su XML y extrayendo información.
|
Audita el pedimento completo (PC): ¿está descargado? ¿se puede procesar?
|
||||||
|
Incluye diagnóstico de campos y errores detectados por tipo de documento.
|
||||||
"""
|
"""
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
|
|
||||||
@@ -1691,8 +1695,9 @@ def auditar_pedimento_endpoint(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Validar permisos y existencia del pedimento
|
pedimento = Pedimento.objects.select_related(
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
'organizacion', 'contribuyente'
|
||||||
|
).get(id=pedimento_id)
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
||||||
@@ -1701,76 +1706,131 @@ def auditar_pedimento_endpoint(request):
|
|||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
# Buscar documentos XML del pedimento
|
# PC descargado (type 2)
|
||||||
documentos_xml = Document.objects.filter(
|
pc_descargado = pedimento.documents.filter(
|
||||||
pedimento=pedimento,
|
document_type_id=2,
|
||||||
archivo__endswith='.xml',
|
|
||||||
organizacion=pedimento.organizacion
|
organizacion=pedimento.organizacion
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
# Fuente de carga
|
||||||
|
fuente = 'datastage' if pedimento.consultar_vucem else 'manual'
|
||||||
|
|
||||||
|
# Diagnóstico de campos
|
||||||
|
aduana = pedimento.aduana or ''
|
||||||
|
patente = pedimento.patente or ''
|
||||||
|
numero_pedimento = pedimento.pedimento or ''
|
||||||
|
|
||||||
|
aduana_valida = bool(aduana) and aduana.isdigit() and 2 <= len(aduana) <= 3
|
||||||
|
patente_valida = bool(patente) and patente.isdigit() and len(patente) == 4
|
||||||
|
pedimento_valido = bool(numero_pedimento) and numero_pedimento.isdigit() and len(numero_pedimento) >= 7
|
||||||
|
numero_operacion_presente = bool(pedimento.numero_operacion)
|
||||||
|
|
||||||
|
from api.vucem.models import CredencialesImportador
|
||||||
|
tiene_contribuyente = pedimento.contribuyente is not None
|
||||||
|
tiene_credenciales = False
|
||||||
|
credenciales_detalle = None
|
||||||
|
if tiene_contribuyente:
|
||||||
|
credencial = CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first()
|
||||||
|
tiene_credenciales = bool(credencial and credencial.vucem)
|
||||||
|
if credencial and not credencial.vucem:
|
||||||
|
credenciales_detalle = 'Credencial encontrada pero sin cuenta VUCEM asociada'
|
||||||
|
elif not credencial:
|
||||||
|
credenciales_detalle = f'Sin credenciales VUCEM para RFC {pedimento.contribuyente.rfc}'
|
||||||
|
|
||||||
|
razones = []
|
||||||
|
if not aduana_valida:
|
||||||
|
razones.append(f'Aduana inválida o ausente (valor: "{aduana}")')
|
||||||
|
if not patente_valida:
|
||||||
|
razones.append(f'Patente inválida o ausente (valor: "{patente}")')
|
||||||
|
if not pedimento_valido:
|
||||||
|
razones.append(f'Número de pedimento inválido (valor: "{numero_pedimento}")')
|
||||||
|
if not tiene_contribuyente:
|
||||||
|
razones.append('Sin contribuyente asignado')
|
||||||
|
elif not tiene_credenciales:
|
||||||
|
razones.append(credenciales_detalle or 'Sin credenciales VUCEM')
|
||||||
|
|
||||||
|
puede_procesar = len(razones) == 0
|
||||||
|
|
||||||
|
datos = {
|
||||||
|
'aduana': aduana or None,
|
||||||
|
'patente': patente or None,
|
||||||
|
'numero_pedimento': numero_pedimento or None,
|
||||||
|
'numero_operacion': pedimento.numero_operacion,
|
||||||
|
'contribuyente_rfc': pedimento.contribuyente.rfc if pedimento.contribuyente else None,
|
||||||
|
'contribuyente_nombre': str(pedimento.contribuyente) if pedimento.contribuyente else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
validacion = {
|
||||||
|
'aduana_valida': aduana_valida,
|
||||||
|
'patente_valida': patente_valida,
|
||||||
|
'pedimento_valido': pedimento_valido,
|
||||||
|
'numero_operacion_presente': numero_operacion_presente,
|
||||||
|
'tiene_contribuyente': tiene_contribuyente,
|
||||||
|
'tiene_credenciales_vucem': tiene_credenciales,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errores por tipo de documento
|
||||||
|
docs_error = (
|
||||||
|
Document.objects
|
||||||
|
.filter(
|
||||||
|
pedimento=pedimento,
|
||||||
|
organizacion=pedimento.organizacion,
|
||||||
|
document_type_id__in=_ERROR_DOCUMENT_TYPES,
|
||||||
|
)
|
||||||
|
.select_related('document_type')
|
||||||
|
.order_by('document_type_id')
|
||||||
)
|
)
|
||||||
|
errores_detectados = [
|
||||||
|
{
|
||||||
|
'documento_id': str(doc.id),
|
||||||
|
'nombre_archivo': os.path.basename(str(doc.archivo)),
|
||||||
|
'tipo_error': doc.document_type.descripcion if doc.document_type else 'Error desconocido',
|
||||||
|
'tipo_id': doc.document_type_id,
|
||||||
|
}
|
||||||
|
for doc in docs_error
|
||||||
|
]
|
||||||
|
|
||||||
if not documentos_xml.exists():
|
# XML del PC si existe
|
||||||
return Response({
|
informacion_xml = None
|
||||||
'pedimento_id': str(pedimento_id),
|
doc_pc = pedimento.documents.filter(
|
||||||
'pedimento': pedimento.pedimento,
|
document_type_id=2,
|
||||||
'pedimento_app': pedimento.pedimento_app,
|
organizacion=pedimento.organizacion,
|
||||||
'archivos_xml_encontrados': 0,
|
archivo__endswith='.xml',
|
||||||
'mensaje': 'No se encontraron archivos XML para este pedimento',
|
).first()
|
||||||
'auditoria_completa': False
|
if doc_pc:
|
||||||
}, status=status.HTTP_200_OK)
|
xml_content = get_document_content(doc_pc)
|
||||||
|
if xml_content:
|
||||||
|
info_pedimento = extraer_info_pedimento_xml(xml_content)
|
||||||
|
if info_pedimento:
|
||||||
|
informacion_xml = info_pedimento
|
||||||
|
actualizar_info_pedimento(pedimento, info_pedimento)
|
||||||
|
|
||||||
# Lista para almacenar información de cada XML
|
hay_pendientes = not pc_descargado
|
||||||
xmls_analizados = []
|
hay_errores = bool(errores_detectados)
|
||||||
informacion_extraida = []
|
|
||||||
|
|
||||||
for documento in documentos_xml:
|
if hay_errores:
|
||||||
|
estado = 'CON_ERRORES'
|
||||||
|
elif hay_pendientes:
|
||||||
|
estado = 'PENDIENTE'
|
||||||
|
else:
|
||||||
|
estado = 'COMPLETO'
|
||||||
|
|
||||||
print(f"documento >>>> {documento}")
|
return Response({
|
||||||
logger.info(f"documento >>>> {documento}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
xml_info = {
|
|
||||||
'documento_id': str(documento.id),
|
|
||||||
'nombre_archivo': os.path.basename(str(documento.archivo)),
|
|
||||||
'tamanio': documento.size,
|
|
||||||
'extension': documento.extension,
|
|
||||||
'tipo_documento': documento.document_type.descripcion if documento.document_type else 'Desconocido'
|
|
||||||
}
|
|
||||||
|
|
||||||
xml_content = get_document_content(documento)
|
|
||||||
|
|
||||||
if xml_content is None:
|
|
||||||
xml_info['error_lectura'] = 'No se pudo descargar el archivo'
|
|
||||||
else:
|
|
||||||
info_pedimento = extraer_info_pedimento_xml(xml_content)
|
|
||||||
|
|
||||||
if info_pedimento:
|
|
||||||
xml_info['informacion_extraida'] = info_pedimento
|
|
||||||
informacion_extraida.append(info_pedimento)
|
|
||||||
|
|
||||||
# Actualizar el pedimento con la información encontrada si es necesario
|
|
||||||
actualizar_info_pedimento(pedimento, info_pedimento)
|
|
||||||
|
|
||||||
xmls_analizados.append(xml_info)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
xmls_analizados.append({
|
|
||||||
'documento_id': str(documento.id),
|
|
||||||
'nombre_archivo': os.path.basename(str(documento.archivo)),
|
|
||||||
'error': f'Error procesando archivo: {str(e)}'
|
|
||||||
})
|
|
||||||
|
|
||||||
response_data = {
|
|
||||||
'pedimento_id': str(pedimento_id),
|
'pedimento_id': str(pedimento_id),
|
||||||
'pedimento': pedimento.pedimento,
|
'pedimento': pedimento.pedimento,
|
||||||
'pedimento_app': pedimento.pedimento_app,
|
'pedimento_app': pedimento.pedimento_app,
|
||||||
'archivos_xml_encontrados': len(xmls_analizados),
|
'estado': estado,
|
||||||
'xmls_analizados': xmls_analizados,
|
'hay_pendientes': hay_pendientes,
|
||||||
'informacion_extraida': informacion_extraida,
|
'hay_errores': hay_errores,
|
||||||
'auditoria_completa': True,
|
'pc_descargado': pc_descargado,
|
||||||
'mensaje': f'Auditoría completada para el pedimento {pedimento.pedimento}'
|
'puede_procesar': puede_procesar,
|
||||||
}
|
'razones_no_puede_procesar': razones,
|
||||||
|
'fuente': fuente,
|
||||||
return Response(response_data, status=status.HTTP_200_OK)
|
'datos': datos,
|
||||||
|
'validacion': validacion,
|
||||||
|
'errores_detectados': errores_detectados,
|
||||||
|
'informacion_xml': informacion_xml,
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except Pedimento.DoesNotExist:
|
except Pedimento.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -1783,6 +1843,164 @@ def auditar_pedimento_endpoint(request):
|
|||||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
method='post',
|
||||||
|
operation_description="Procesa el pedimento completo (tipo 2) de un pedimento específico llamando al microservicio VUCEM.",
|
||||||
|
request_body=openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
'pedimento_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID del pedimento'),
|
||||||
|
},
|
||||||
|
required=['pedimento_id']
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
202: openapi.Response('Procesamiento encolado — usar task_id para consultar resultado'),
|
||||||
|
200: openapi.Response('El pedimento ya tiene su documento completo descargado'),
|
||||||
|
400: openapi.Response('Error en los parámetros o prerequisitos faltantes'),
|
||||||
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
|
404: openapi.Response('Pedimento no encontrado'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api_view(['POST'])
|
||||||
|
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
|
||||||
|
def procesar_pedimento_completo_endpoint(request):
|
||||||
|
"""
|
||||||
|
Diagnostica el pedimento completo y, si todo está en orden y aún no se ha
|
||||||
|
descargado, encola la tarea de procesamiento.
|
||||||
|
|
||||||
|
Siempre devuelve diagnóstico completo: validación de campos, fuente, estado
|
||||||
|
del PC y razones por las que no se puede procesar si aplica.
|
||||||
|
"""
|
||||||
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
|
|
||||||
|
if not pedimento_id:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Debe proporcionar pedimento_id'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pedimento = Pedimento.objects.select_related(
|
||||||
|
'organizacion', 'contribuyente', 'tipo_operacion'
|
||||||
|
).get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
||||||
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# --- Diagnóstico de campos ---
|
||||||
|
aduana = pedimento.aduana or ''
|
||||||
|
patente = pedimento.patente or ''
|
||||||
|
numero_pedimento = pedimento.pedimento or ''
|
||||||
|
|
||||||
|
aduana_valida = bool(aduana) and aduana.isdigit() and 2 <= len(aduana) <= 3
|
||||||
|
patente_valida = bool(patente) and patente.isdigit() and len(patente) == 4
|
||||||
|
pedimento_valido = bool(numero_pedimento) and numero_pedimento.isdigit() and len(numero_pedimento) >= 7
|
||||||
|
numero_operacion_presente = bool(pedimento.numero_operacion)
|
||||||
|
|
||||||
|
# --- Fuente de carga ---
|
||||||
|
# consultar_vucem=True indica que fue originado desde datastage
|
||||||
|
fuente = 'datastage' if pedimento.consultar_vucem else 'manual'
|
||||||
|
|
||||||
|
# --- Estado del PC ---
|
||||||
|
pc_descargado = pedimento.documents.filter(
|
||||||
|
document_type_id=2,
|
||||||
|
organizacion=pedimento.organizacion
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
# --- Credenciales VUCEM ---
|
||||||
|
from api.vucem.models import CredencialesImportador
|
||||||
|
tiene_contribuyente = pedimento.contribuyente is not None
|
||||||
|
tiene_credenciales = False
|
||||||
|
credenciales_detalle = None
|
||||||
|
if tiene_contribuyente:
|
||||||
|
credencial = CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first()
|
||||||
|
tiene_credenciales = bool(credencial and credencial.vucem)
|
||||||
|
if credencial and not credencial.vucem:
|
||||||
|
credenciales_detalle = 'Credencial encontrada pero sin cuenta VUCEM asociada'
|
||||||
|
elif not credencial:
|
||||||
|
credenciales_detalle = f'Sin credenciales VUCEM para RFC {pedimento.contribuyente.rfc}'
|
||||||
|
|
||||||
|
# --- Puede procesar ---
|
||||||
|
razones = []
|
||||||
|
if not aduana_valida:
|
||||||
|
razones.append(f'Aduana inválida o ausente (valor: "{aduana}")')
|
||||||
|
if not patente_valida:
|
||||||
|
razones.append(f'Patente inválida o ausente (valor: "{patente}")')
|
||||||
|
if not pedimento_valido:
|
||||||
|
razones.append(f'Número de pedimento inválido (valor: "{numero_pedimento}")')
|
||||||
|
if not tiene_contribuyente:
|
||||||
|
razones.append('Sin contribuyente asignado')
|
||||||
|
elif not tiene_credenciales:
|
||||||
|
razones.append(credenciales_detalle or 'Sin credenciales VUCEM')
|
||||||
|
|
||||||
|
puede_procesar = len(razones) == 0
|
||||||
|
|
||||||
|
datos = {
|
||||||
|
'aduana': aduana or None,
|
||||||
|
'patente': patente or None,
|
||||||
|
'numero_pedimento': numero_pedimento or None,
|
||||||
|
'numero_operacion': pedimento.numero_operacion,
|
||||||
|
'regimen': pedimento.regimen,
|
||||||
|
'clave_pedimento': pedimento.clave_pedimento,
|
||||||
|
'fecha_pago': str(pedimento.fecha_pago) if pedimento.fecha_pago else None,
|
||||||
|
'contribuyente_rfc': pedimento.contribuyente.rfc if pedimento.contribuyente else None,
|
||||||
|
'contribuyente_nombre': str(pedimento.contribuyente) if pedimento.contribuyente else None,
|
||||||
|
'remesas': pedimento.remesas,
|
||||||
|
'numero_partidas': pedimento.numero_partidas,
|
||||||
|
}
|
||||||
|
|
||||||
|
validacion = {
|
||||||
|
'aduana_valida': aduana_valida,
|
||||||
|
'patente_valida': patente_valida,
|
||||||
|
'pedimento_valido': pedimento_valido,
|
||||||
|
'numero_operacion_presente': numero_operacion_presente,
|
||||||
|
'tiene_contribuyente': tiene_contribuyente,
|
||||||
|
'tiene_credenciales_vucem': tiene_credenciales,
|
||||||
|
'puede_procesar': puede_procesar,
|
||||||
|
'razones_no_puede_procesar': razones,
|
||||||
|
}
|
||||||
|
|
||||||
|
base_response = {
|
||||||
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'pedimento_app': pedimento.pedimento_app,
|
||||||
|
'fuente': fuente,
|
||||||
|
'datos': datos,
|
||||||
|
'validacion': validacion,
|
||||||
|
'pc_descargado': pc_descargado,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ya descargado — devolver diagnóstico sin encolar
|
||||||
|
if pc_descargado:
|
||||||
|
return Response({
|
||||||
|
**base_response,
|
||||||
|
'estado': 'ya_descargado',
|
||||||
|
'mensaje': 'El pedimento completo ya fue descargado',
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# No puede procesar — devolver diagnóstico con razones
|
||||||
|
if not puede_procesar:
|
||||||
|
return Response({
|
||||||
|
**base_response,
|
||||||
|
'estado': 'no_puede_procesar',
|
||||||
|
'mensaje': 'El pedimento no cumple los requisitos para procesar',
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# Todo en orden — encolar
|
||||||
|
task = procesar_pedimento_completo_individual.delay(str(pedimento_id))
|
||||||
|
logger.info(f"Procesamiento PC encolado: {pedimento.pedimento} (task={task.id})")
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
**base_response,
|
||||||
|
'estado': 'encolado',
|
||||||
|
'task_id': task.id,
|
||||||
|
'mensaje': f'Procesamiento encolado para {pedimento.pedimento_app}',
|
||||||
|
}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
|
||||||
def actualizar_info_pedimento(pedimento, info_xml):
|
def actualizar_info_pedimento(pedimento, info_xml):
|
||||||
"""
|
"""
|
||||||
Actualiza la información del pedimento con los datos extraídos del XML.
|
Actualiza la información del pedimento con los datos extraídos del XML.
|
||||||
|
|||||||
@@ -12,106 +12,73 @@ from rest_framework.decorators import action
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.http import FileResponse, Http404
|
from django.http import FileResponse, Http404
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .models import DataStage
|
from .models import DataStage
|
||||||
from .serializer import DataStageSerializer
|
from .serializer import DataStageSerializer
|
||||||
|
|
||||||
from api.logger.mixins import LoggingMixin
|
from api.logger.mixins import LoggingMixin
|
||||||
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
|
from core.permissions import get_org_context, is_internal_service_request, require_permission
|
||||||
from core.permissions import (
|
|
||||||
IsSameOrganization,
|
|
||||||
IsSameOrganizationDeveloper,
|
|
||||||
IsSameOrganizationAndAdmin,
|
|
||||||
IsSuperUser
|
|
||||||
)
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
class DataStagePagination(PageNumberPagination):
|
class DataStagePagination(PageNumberPagination):
|
||||||
page_size = 20 # Valor por defecto
|
page_size = 20 # Valor por defecto
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
max_page_size = 1000
|
max_page_size = 1000
|
||||||
|
|
||||||
class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ViewSet for managing DataStage instances.
|
ViewSet for managing DataStage instances.
|
||||||
Provides CRUD operations for DataStage.
|
Provides CRUD operations for DataStage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
serializer_class = DataStageSerializer
|
serializer_class = DataStageSerializer
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
model = DataStage
|
model = DataStage
|
||||||
my_tags = ['DataStage']
|
my_tags = ['DataStage']
|
||||||
pagination_class = DataStagePagination
|
pagination_class = DataStagePagination
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
perms = {
|
||||||
|
'list': 'datastage.view',
|
||||||
|
'retrieve': 'datastage.view',
|
||||||
|
'create': 'datastage.create',
|
||||||
|
'update': 'datastage.create',
|
||||||
|
'partial_update': 'datastage.create',
|
||||||
|
'destroy': 'datastage.delete',
|
||||||
|
'procesar': 'datastage.process',
|
||||||
|
'download_datastage': 'datastage.view',
|
||||||
|
'task_status': 'datastage.view',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'datastage.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_superuser:
|
if is_internal_service_request(self.request):
|
||||||
return DataStage.objects.all().order_by('-created_at')
|
return DataStage.objects.all().order_by('-created_at')
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
if self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='Agente Aduanal').exists():
|
if not org:
|
||||||
return DataStage.objects.filter(organizacion=self.request.user.organizacion).order_by('-created_at')
|
return DataStage.objects.none()
|
||||||
|
return DataStage.objects.filter(organizacion=org).order_by('-created_at')
|
||||||
return self.get_queryset_filtrado_por_organizacion().order_by('-created_at')
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""
|
org = get_org_context(self.request.user)
|
||||||
Permite que la organización sea opcional en el request, pero si no se envía, se asigna la del usuario autenticado.
|
datastage = serializer.save(organizacion=org)
|
||||||
"""
|
self._trigger_processing(datastage)
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
|
||||||
raise ValueError("Usuario no autenticado o sin organización")
|
|
||||||
|
|
||||||
data = serializer.validated_data
|
|
||||||
organizacion = data.get('organizacion')
|
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
# Permitir que el superusuario cree sin organización o la especifique
|
|
||||||
datastage = serializer.save()
|
|
||||||
self._trigger_processing(datastage)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
|
||||||
if not organizacion:
|
|
||||||
datastage = serializer.save(organizacion=self.request.user.organizacion)
|
|
||||||
else:
|
|
||||||
datastage = serializer.save()
|
|
||||||
|
|
||||||
self._trigger_processing(datastage)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ValueError("No cuentas con los permisos necesarios para crear un DataStage")
|
|
||||||
|
|
||||||
def _trigger_processing(self, datastage):
|
def _trigger_processing(self, datastage):
|
||||||
"""
|
|
||||||
Método helper para disparar el procesamiento.
|
|
||||||
"""
|
|
||||||
from api.datastage.tasks import procesar_datastage_task
|
from api.datastage.tasks import procesar_datastage_task
|
||||||
user_organizacion = getattr(self.request.user, 'organizacion', None)
|
org = get_org_context(self.request.user)
|
||||||
user_organizacion_id = user_organizacion.id if user_organizacion else None
|
|
||||||
|
|
||||||
datastage.procesado = True
|
datastage.procesado = True
|
||||||
datastage.save()
|
datastage.save()
|
||||||
|
procesar_datastage_task.delay(datastage.id, org.id if org else None)
|
||||||
task = procesar_datastage_task.delay(datastage.id, user_organizacion_id)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""
|
if is_internal_service_request(self.request):
|
||||||
Override to ensure organization is set on update.
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
|
||||||
raise ValueError("Usuario no autenticado o sin organización")
|
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
# Allow superuser to update without organization
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
serializer.save(organizacion=org)
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
def perform_destroy(self, instance):
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
if instance.archivo:
|
||||||
return
|
storage_service.delete_file(instance.archivo)
|
||||||
|
instance.delete()
|
||||||
raise ValueError("No cuentas con los permisos necesarios para actualizar un DataStage")
|
|
||||||
|
|
||||||
@action(detail=True, methods=['get'], url_path='download-datastage', url_name='download-datastage')
|
@action(detail=True, methods=['get'], url_path='download-datastage', url_name='download-datastage')
|
||||||
def download_datastage(self, request, pk=None):
|
def download_datastage(self, request, pk=None):
|
||||||
@@ -182,12 +149,10 @@ class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
|||||||
"""
|
"""
|
||||||
Endpoint para procesar el DataStage de forma asíncrona usando Celery.
|
Endpoint para procesar el DataStage de forma asíncrona usando Celery.
|
||||||
"""
|
"""
|
||||||
# ojo aqui
|
|
||||||
from api.datastage.tasks import procesar_datastage_task
|
from api.datastage.tasks import procesar_datastage_task
|
||||||
datastage = self.get_object()
|
datastage = self.get_object()
|
||||||
user_organizacion = getattr(self.request.user, 'organizacion', None)
|
org = get_org_context(self.request.user)
|
||||||
user_organizacion_id = user_organizacion.id if user_organizacion else None
|
task = procesar_datastage_task.delay(datastage.id, org.id if org else None)
|
||||||
task = procesar_datastage_task.delay(datastage.id, user_organizacion_id)
|
|
||||||
return Response({
|
return Response({
|
||||||
'task_id': task.id,
|
'task_id': task.id,
|
||||||
'detail': 'Procesamiento iniciado. Puede consultar el estado con el task_id.'
|
'detail': 'Procesamiento iniciado. Puede consultar el estado con el task_id.'
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ class UserActivityViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return UserActivity.objects.none()
|
return UserActivity.objects.none()
|
||||||
|
|
||||||
# Los usuarios normales solo ven su propia actividad
|
if self.request.user.is_superuser:
|
||||||
if self.request.user.is_staff:
|
|
||||||
return UserActivity.objects.all()
|
return UserActivity.objects.all()
|
||||||
return UserActivity.objects.filter(user=self.request.user)
|
return UserActivity.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
|||||||
@@ -4,31 +4,43 @@ from django.dispatch import receiver
|
|||||||
from api.notificaciones.models import Notificacion
|
from api.notificaciones.models import Notificacion
|
||||||
from api.record.models import Document
|
from api.record.models import Document
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Document)
|
@receiver(post_save, sender=Document)
|
||||||
def trigger_notificacion(sender, instance, created, **kwargs):
|
def trigger_notificacion(sender, instance, created, **kwargs):
|
||||||
if created:
|
if not created:
|
||||||
from api.cuser.models import CustomUser
|
return
|
||||||
from api.customs.models import Pedimento
|
|
||||||
from api.notificaciones.models import TipoNotificacion
|
|
||||||
|
|
||||||
# Obtener el tipo de notificación (puedes ajustar el nombre si tienes tipos definidos)
|
from api.cuser.models import CustomUser
|
||||||
tipo_info, _ = TipoNotificacion.objects.get_or_create(tipo="info", defaults={"descripcion": "Notificación informativa"})
|
from api.notificaciones.models import TipoNotificacion
|
||||||
|
from core.permissions import user_has_permission
|
||||||
|
|
||||||
# Notificar a todos los usuarios de la organización
|
tipo_info, _ = TipoNotificacion.objects.get_or_create(
|
||||||
usuarios_org = CustomUser.objects.filter(organizacion=instance.organizacion)
|
tipo='info',
|
||||||
for usuario in usuarios_org:
|
defaults={'descripcion': 'Notificación informativa'},
|
||||||
# Notificar solo a importadores cuyo RFC coincide
|
)
|
||||||
if (usuario.is_importador or usuario.groups.filter(name='Importador').exists()):
|
|
||||||
if instance.pedimento.contribuyente in usuario.rfc.all():
|
mensaje = (
|
||||||
Notificacion.objects.create(
|
f"Se agregó el documento {instance.archivo} "
|
||||||
tipo=tipo_info,
|
f"al pedimento {instance.pedimento.pedimento}\n"
|
||||||
dirigido=usuario,
|
f"{instance.document_type.nombre}"
|
||||||
mensaje=f"Se agregó el documento {instance.archivo} al pedimento {instance.pedimento.pedimento} \n {instance.document_type.nombre}",
|
)
|
||||||
)
|
|
||||||
# Notificar a otros roles (no importadores)
|
usuarios_org = CustomUser.objects.filter(
|
||||||
elif (usuario.is_superuser or usuario.groups.filter(name='Agente Aduanal').exists() or usuario.groups.filter(name='admin').exists()):
|
organizacion=instance.organizacion,
|
||||||
Notificacion.objects.create(
|
is_active=True,
|
||||||
tipo=tipo_info,
|
).prefetch_related('rfc')
|
||||||
dirigido=usuario,
|
|
||||||
mensaje=f"Se agregó el documento {instance.archivo} al pedimento {instance.pedimento.pedimento} \n {instance.document_type.nombre}",
|
for usuario in usuarios_org:
|
||||||
)
|
if not user_has_permission(usuario, 'notificaciones.receive'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Importadores: solo si el pedimento corresponde a uno de sus RFC
|
||||||
|
if usuario.is_importador:
|
||||||
|
if instance.pedimento.contribuyente not in usuario.rfc.all():
|
||||||
|
continue
|
||||||
|
|
||||||
|
Notificacion.objects.create(
|
||||||
|
tipo=tipo_info,
|
||||||
|
dirigido=usuario,
|
||||||
|
mensaje=mensaje,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from .models import Notificacion, TipoNotificacion
|
from .models import Notificacion, TipoNotificacion
|
||||||
from .serializers import NotificacionSerializer, TipoNotificacionSerializer
|
from .serializers import NotificacionSerializer, TipoNotificacionSerializer
|
||||||
from core.permissions import (
|
from core.permissions import require_permission
|
||||||
IsSameOrganization,
|
|
||||||
IsSameOrganizationDeveloper,
|
|
||||||
IsSameOrganizationAndAdmin,
|
|
||||||
IsSuperUser
|
|
||||||
)
|
|
||||||
# Create your views here.
|
|
||||||
|
|
||||||
class TipoNotificacionViewSet(viewsets.ModelViewSet):
|
class TipoNotificacionViewSet(viewsets.ModelViewSet):
|
||||||
queryset = TipoNotificacion.objects.all()
|
queryset = TipoNotificacion.objects.all()
|
||||||
serializer_class = TipoNotificacionSerializer
|
serializer_class = TipoNotificacionSerializer
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
|
|
||||||
my_tags = ['Notificaciones']
|
my_tags = ['Notificaciones']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.queryset.order_by('tipo')
|
return self.queryset.order_by('tipo')
|
||||||
|
|
||||||
|
|
||||||
class NotificacionViewSet(viewsets.ModelViewSet):
|
class NotificacionViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Notificacion.objects.all()
|
queryset = Notificacion.objects.all()
|
||||||
serializer_class = NotificacionSerializer
|
serializer_class = NotificacionSerializer
|
||||||
http_method_names = ['get', 'post', 'put', 'patch', 'delete']
|
http_method_names = ['get', 'post', 'put', 'patch', 'delete']
|
||||||
filterset_fields = ['visto']
|
filterset_fields = ['visto']
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
my_tags = ['Notificaciones']
|
my_tags = ['Notificaciones']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
return [IsAuthenticated(), require_permission('notificaciones.view')()]
|
||||||
|
return [IsAuthenticated()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Evita error en generación de esquema Swagger
|
|
||||||
if getattr(self, 'swagger_fake_view', False):
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
return Notificacion.objects.none()
|
return Notificacion.objects.none()
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
@@ -45,6 +42,6 @@ class NotificacionViewSet(viewsets.ModelViewSet):
|
|||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
raise PermissionDenied("Usuario no autenticado")
|
raise PermissionDenied("Usuario no autenticado")
|
||||||
if self.request.user.is_superuser:
|
if self.request.user.is_superuser:
|
||||||
# Allow superusers and admins to create notifications for any user
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
return
|
||||||
raise PermissionDenied("No tienes permiso para crear notificaciones para otros usuarios")
|
raise PermissionDenied("No tienes permiso para crear notificaciones para otros usuarios")
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Organizacion
|
from .models import Organizacion
|
||||||
# Register your models here.
|
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Organizacion)
|
||||||
class OrganizacionAdmin(admin.ModelAdmin):
|
class OrganizacionAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'nombre', 'rfc', 'email', 'telefono', 'is_active', 'is_verified', 'inicia', 'vencimiento')
|
list_display = ('nombre', 'rfc', 'email', 'telefono', 'owner', 'is_active', 'is_verified', 'inicio', 'vencimiento')
|
||||||
search_fields = ('nombre', 'rfc', 'email')
|
search_fields = ('nombre', 'rfc', 'email')
|
||||||
list_filter = ('is_active', 'is_verified')
|
list_filter = ('is_active', 'is_verified', 'is_agente_aduanal')
|
||||||
ordering = ('nombre',)
|
ordering = ('nombre',)
|
||||||
|
autocomplete_fields = ('owner',)
|
||||||
# class UsuarioOrganizacionAdmin(admin.ModelAdmin):
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
# list_display = ('id', 'email', 'telefono', 'puesto', 'is_active', 'is_verified')
|
fieldsets = (
|
||||||
# search_fields = ('email', 'telefono', 'puesto')
|
(None, {'fields': ('nombre', 'rfc', 'titular', 'licencia')}),
|
||||||
# list_filter = ('is_active', 'is_verified')
|
('Contacto', {'fields': ('email', 'telefono', 'estado', 'ciudad')}),
|
||||||
# ordering = ('email',)
|
('Administrador maestro', {'fields': ('owner',)}),
|
||||||
|
('Estado', {'fields': ('is_active', 'is_verified', 'is_agente_aduanal', 'apply_auto_download')}),
|
||||||
admin.site.register(Organizacion)
|
('Vigencia', {'fields': ('inicio', 'vencimiento')}),
|
||||||
# admin.site.register(UsuarioOrganizacion)
|
('Observaciones', {'fields': ('observaciones',)}),
|
||||||
|
('Auditoría', {'fields': ('created_at', 'updated_at')}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ class Organizacion(models.Model):
|
|||||||
estado = models.CharField(max_length=50)
|
estado = models.CharField(max_length=50)
|
||||||
ciudad = models.CharField(max_length=50)
|
ciudad = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
# Administrador maestro: acceso total a su org, no puede ser removido de su rol por otros admins.
|
||||||
|
# on_delete=PROTECT: no se puede eliminar el usuario sin reasignar el ownership primero.
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
'cuser.CustomUser',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='organizaciones_que_administra',
|
||||||
|
)
|
||||||
|
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
is_verified = models.BooleanField(default=False)
|
is_verified = models.BooleanField(default=False)
|
||||||
apply_auto_download = models.BooleanField(default=False)
|
apply_auto_download = models.BooleanField(default=False)
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .models import Organizacion, UsoAlmacenamiento
|
from .models import Organizacion, UsoAlmacenamiento
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Organizacion)
|
@receiver(post_save, sender=Organizacion)
|
||||||
def crear_uso_almacenamiento(sender, instance, created, **kwargs):
|
def crear_uso_almacenamiento(sender, instance, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
UsoAlmacenamiento.objects.create(organizacion=instance, espacio_utilizado=0)
|
UsoAlmacenamiento.objects.create(organizacion=instance, espacio_utilizado=0)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Organizacion)
|
||||||
|
def crear_roles_default(sender, instance, created, **kwargs):
|
||||||
|
"""Al crear una organización nueva, genera automáticamente los 5 roles por defecto
|
||||||
|
con sus permisos. Depende de que el catálogo RolePermission ya exista (post-migration)."""
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from api.rbac.roles import crear_roles_para_organizacion
|
||||||
|
crear_roles_para_organizacion(instance)
|
||||||
|
except Exception:
|
||||||
|
# Si la app rbac aún no está migrada (ej. primer deploy), no bloquear la creación de org
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).warning(
|
||||||
|
'No se pudieron crear roles para org %s — verifica que rbac esté migrado.',
|
||||||
|
instance.id,
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ from core.permissions import (
|
|||||||
IsSameOrganization,
|
IsSameOrganization,
|
||||||
IsSameOrganizationDeveloper,
|
IsSameOrganizationDeveloper,
|
||||||
IsSameOrganizationAndAdmin,
|
IsSameOrganizationAndAdmin,
|
||||||
IsSuperUser
|
IsSuperUser,
|
||||||
|
get_org_context,
|
||||||
|
is_internal_service_request,
|
||||||
|
user_has_permission,
|
||||||
)
|
)
|
||||||
from .serializers import OrganizacionSerializer, UsoAlmacenamientoSerializer
|
from .serializers import OrganizacionSerializer, UsoAlmacenamientoSerializer
|
||||||
from .models import Organizacion, UsoAlmacenamiento
|
from .models import Organizacion, UsoAlmacenamiento
|
||||||
@@ -32,21 +35,19 @@ class ViewSetOrganizacion(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltr
|
|||||||
my_tags = ['Organizaciones']
|
my_tags = ['Organizaciones']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
user = self.request.user
|
||||||
|
if not user.is_authenticated:
|
||||||
return Organizacion.objects.none()
|
return Organizacion.objects.none()
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
if is_internal_service_request(self.request):
|
||||||
# Superuser can see all organizations
|
|
||||||
return Organizacion.objects.all()
|
return Organizacion.objects.all()
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter('developer').exists() or self.request.user.groups.filter('user')) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
org = get_org_context(user)
|
||||||
# Importers can only see their own organization
|
if not org:
|
||||||
return Organizacion.objects.filter(users=self.request.user)
|
return Organizacion.objects.none()
|
||||||
|
|
||||||
if self.request.user.groups.filter(name='importador').exists():
|
# Superuser ve solo su org activa, no todas
|
||||||
return Organizacion.objects.filter(users=self.request.user)
|
return Organizacion.objects.filter(id=org.id)
|
||||||
|
|
||||||
return Organizacion.objects.none()
|
|
||||||
|
|
||||||
class UsoAlmacenamientoViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
|
class UsoAlmacenamientoViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
@@ -60,31 +61,26 @@ class UsoAlmacenamientoViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
my_tags = ['Uso de Almacenamiento']
|
my_tags = ['Uso de Almacenamiento']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if not self.request.user.is_authenticated:
|
||||||
return UsoAlmacenamiento.objects.none()
|
return UsoAlmacenamiento.objects.none()
|
||||||
|
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
if self.request.user.is_superuser:
|
|
||||||
# Superuser can see all storage usage
|
|
||||||
return UsoAlmacenamiento.objects.all()
|
return UsoAlmacenamiento.objects.all()
|
||||||
|
|
||||||
if (self.request.user.groups.filter(name='developer').exists() or
|
org = get_org_context(self.request.user)
|
||||||
self.request.user.groups.filter(name='admin').exists() or
|
if not org:
|
||||||
self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
|
return UsoAlmacenamiento.objects.none()
|
||||||
# Developers, Admins, and Users can see their organization's storage usage
|
|
||||||
return UsoAlmacenamiento.objects.filter(organizacion=self.request.user.organizacion)
|
|
||||||
|
|
||||||
if self.request.user.groups.filter(name='importador').exists():
|
if self.request.user.is_importador:
|
||||||
# Importers can only see their own organization's storage usage
|
|
||||||
raise PermissionDenied("Los importadores no tienen acceso al uso de almacenamiento.")
|
raise PermissionDenied("Los importadores no tienen acceso al uso de almacenamiento.")
|
||||||
|
|
||||||
return UsoAlmacenamiento.objects.none()
|
return UsoAlmacenamiento.objects.filter(organizacion=org)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def mi_organizacion(self, request):
|
def mi_organizacion(self, request):
|
||||||
|
|
||||||
"""Obtiene el uso de almacenamiento de la organización del usuario actual"""
|
"""Obtiene el uso de almacenamiento de la organización del usuario actual"""
|
||||||
organizacion = request.user.organizacion
|
organizacion = get_org_context(request.user)
|
||||||
|
|
||||||
# Obtener o crear el registro de uso
|
# Obtener o crear el registro de uso
|
||||||
uso, created = UsoAlmacenamiento.objects.get_or_create(
|
uso, created = UsoAlmacenamiento.objects.get_or_create(
|
||||||
|
|||||||
0
api/rbac/__init__.py
Normal file
0
api/rbac/__init__.py
Normal file
99
api/rbac/admin.py
Normal file
99
api/rbac/admin.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import OrganizationRole, RolePermission, UserPermission, UserRole
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(RolePermission)
|
||||||
|
class RolePermissionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('codename', 'modulo', 'descripcion')
|
||||||
|
list_filter = ('modulo',)
|
||||||
|
search_fields = ('codename', 'descripcion')
|
||||||
|
ordering = ('modulo', 'codename')
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
# Al editar un permiso existente los campos son readonly para evitar inconsistencias
|
||||||
|
if obj:
|
||||||
|
return ('codename', 'modulo', 'descripcion')
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
class UserRoleInline(admin.TabularInline):
|
||||||
|
model = UserRole
|
||||||
|
extra = 0
|
||||||
|
autocomplete_fields = ('user',)
|
||||||
|
readonly_fields = ('created_at',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(OrganizationRole)
|
||||||
|
class OrganizationRoleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('nombre', 'organizacion', 'is_admin_role', 'permisos_count', 'usuarios_count')
|
||||||
|
list_filter = ('organizacion', 'is_admin_role')
|
||||||
|
search_fields = ('nombre', 'organizacion__nombre')
|
||||||
|
filter_horizontal = ('permissions',)
|
||||||
|
inlines = (UserRoleInline,)
|
||||||
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
|
def permisos_count(self, obj):
|
||||||
|
return obj.permissions.count()
|
||||||
|
permisos_count.short_description = 'Permisos'
|
||||||
|
|
||||||
|
def usuarios_count(self, obj):
|
||||||
|
return obj.user_roles.count()
|
||||||
|
usuarios_count.short_description = 'Usuarios'
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
if obj and obj.is_admin_role:
|
||||||
|
return False
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserRole)
|
||||||
|
class UserRoleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'role', 'organizacion', 'created_at')
|
||||||
|
list_filter = ('role__organizacion', 'role__nombre')
|
||||||
|
search_fields = ('user__username', 'user__email', 'role__nombre')
|
||||||
|
autocomplete_fields = ('user',)
|
||||||
|
readonly_fields = ('created_at',)
|
||||||
|
|
||||||
|
def organizacion(self, obj):
|
||||||
|
return obj.role.organizacion
|
||||||
|
organizacion.short_description = 'Organización'
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
# Bloquear remoción del rol admin_role al owner de la org
|
||||||
|
if change and obj.role.is_admin_role:
|
||||||
|
org = obj.role.organizacion
|
||||||
|
if hasattr(org, 'owner') and org.owner == obj.user:
|
||||||
|
from django.contrib import messages
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
'No se puede remover el rol de administrador maestro al owner de la organización.',
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserPermission)
|
||||||
|
class UserPermissionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'permission', 'granted', 'organizacion', 'created_at')
|
||||||
|
list_filter = ('granted', 'permission__modulo')
|
||||||
|
search_fields = ('user__username', 'user__email', 'permission__codename')
|
||||||
|
autocomplete_fields = ('user',)
|
||||||
|
readonly_fields = ('created_at',)
|
||||||
|
|
||||||
|
def organizacion(self, obj):
|
||||||
|
return getattr(obj.user, 'organizacion', '—')
|
||||||
|
organizacion.short_description = 'Organización'
|
||||||
8
api/rbac/apps.py
Normal file
8
api/rbac/apps.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RbacConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'api.rbac'
|
||||||
|
label = 'rbac'
|
||||||
|
verbose_name = 'RBAC'
|
||||||
0
api/rbac/management/__init__.py
Normal file
0
api/rbac/management/__init__.py
Normal file
0
api/rbac/management/commands/__init__.py
Normal file
0
api/rbac/management/commands/__init__.py
Normal file
101
api/rbac/management/commands/sync_rbac.py
Normal file
101
api/rbac/management/commands/sync_rbac.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Sincroniza el catálogo de permisos de roles.py con la base de datos.
|
||||||
|
|
||||||
|
Uso básico (solo catálogo):
|
||||||
|
python manage.py sync_rbac
|
||||||
|
|
||||||
|
Con propagación a roles existentes (agrega permisos nuevos a roles que ya existen):
|
||||||
|
python manage.py sync_rbac --roles
|
||||||
|
|
||||||
|
Con listado de lo que hay actualmente:
|
||||||
|
python manage.py sync_rbac --list
|
||||||
|
"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from api.rbac.roles import DEFAULT_ROLES, PERMISSIONS_CATALOG
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Sincroniza el catálogo de permisos (roles.py → BD) sin necesidad de migración.'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--roles',
|
||||||
|
action='store_true',
|
||||||
|
help='Propaga los permisos nuevos a los OrganizationRoles existentes que coincidan con DEFAULT_ROLES.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--list',
|
||||||
|
action='store_true',
|
||||||
|
help='Lista los permisos actuales en la BD agrupados por módulo.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
from api.rbac.models import OrganizationRole, RolePermission
|
||||||
|
|
||||||
|
if options['list']:
|
||||||
|
self._list_permisos(RolePermission)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._sync_catalogo(RolePermission)
|
||||||
|
|
||||||
|
if options['roles']:
|
||||||
|
self._sync_roles(RolePermission, OrganizationRole)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _sync_catalogo(self, RolePermission):
|
||||||
|
creados = 0
|
||||||
|
existentes = 0
|
||||||
|
|
||||||
|
for codename, descripcion, modulo in PERMISSIONS_CATALOG:
|
||||||
|
_, created = RolePermission.objects.get_or_create(
|
||||||
|
codename=codename,
|
||||||
|
defaults={'descripcion': descripcion, 'modulo': modulo},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS(f' [+] {codename} ({modulo})'))
|
||||||
|
creados += 1
|
||||||
|
else:
|
||||||
|
existentes += 1
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'\nCatálogo: {creados} permisos creados, {existentes} ya existían.')
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sync_roles(self, RolePermission, OrganizationRole):
|
||||||
|
perms_map = {p.codename: p for p in RolePermission.objects.all()}
|
||||||
|
roles_actualizados = 0
|
||||||
|
permisos_agregados = 0
|
||||||
|
|
||||||
|
for org_role in OrganizationRole.objects.select_related('organizacion').prefetch_related('permissions'):
|
||||||
|
config = DEFAULT_ROLES.get(org_role.nombre)
|
||||||
|
if not config:
|
||||||
|
continue
|
||||||
|
|
||||||
|
esperados = {c: perms_map[c] for c in config['permissions'] if c in perms_map}
|
||||||
|
actuales = {p.codename for p in org_role.permissions.all()}
|
||||||
|
nuevos = {c: p for c, p in esperados.items() if c not in actuales}
|
||||||
|
|
||||||
|
if nuevos:
|
||||||
|
org_role.permissions.add(*nuevos.values())
|
||||||
|
roles_actualizados += 1
|
||||||
|
permisos_agregados += len(nuevos)
|
||||||
|
self.stdout.write(
|
||||||
|
f' Rol "{org_role.nombre}" en {org_role.organizacion}: '
|
||||||
|
f'+{len(nuevos)} → {", ".join(nuevos.keys())}'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f'\nRoles: {roles_actualizados} roles actualizados, {permisos_agregados} asignaciones nuevas.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _list_permisos(self, RolePermission):
|
||||||
|
modulo_actual = None
|
||||||
|
for perm in RolePermission.objects.order_by('modulo', 'codename'):
|
||||||
|
if perm.modulo != modulo_actual:
|
||||||
|
modulo_actual = perm.modulo
|
||||||
|
self.stdout.write(self.style.HTTP_INFO(f'\n {modulo_actual}'))
|
||||||
|
self.stdout.write(f' {perm.codename:<40} {perm.descripcion}')
|
||||||
116
api/rbac/migrations/0001_initial.py
Normal file
116
api/rbac/migrations/0001_initial.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import uuid
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('organization', '0003_organizacion_apply_auto_download'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RolePermission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('codename', models.CharField(max_length=100, unique=True)),
|
||||||
|
('descripcion', models.CharField(max_length=255)),
|
||||||
|
('modulo', models.CharField(max_length=50)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Permiso',
|
||||||
|
'verbose_name_plural': 'Permisos',
|
||||||
|
'db_table': 'rbac_role_permission',
|
||||||
|
'ordering': ['modulo', 'codename'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrganizationRole',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('nombre', models.CharField(max_length=100)),
|
||||||
|
('descripcion', models.CharField(blank=True, max_length=255)),
|
||||||
|
('is_admin_role', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('organizacion', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='roles',
|
||||||
|
to='organization.organizacion',
|
||||||
|
)),
|
||||||
|
('permissions', models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name='roles',
|
||||||
|
to='rbac.rolepermission',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Rol de Organización',
|
||||||
|
'verbose_name_plural': 'Roles de Organización',
|
||||||
|
'db_table': 'rbac_organization_role',
|
||||||
|
'ordering': ['nombre'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='organizationrole',
|
||||||
|
constraint=models.UniqueConstraint(fields=['organizacion', 'nombre'], name='unique_role_per_org'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserRole',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='user_roles',
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
)),
|
||||||
|
('role', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='user_roles',
|
||||||
|
to='rbac.organizationrole',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Rol de Usuario',
|
||||||
|
'verbose_name_plural': 'Roles de Usuario',
|
||||||
|
'db_table': 'rbac_user_role',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='userrole',
|
||||||
|
constraint=models.UniqueConstraint(fields=['user', 'role'], name='unique_user_role'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserPermission',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('granted', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='rbac_permissions',
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
)),
|
||||||
|
('permission', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='user_overrides',
|
||||||
|
to='rbac.rolepermission',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Permiso Singular',
|
||||||
|
'verbose_name_plural': 'Permisos Singulares',
|
||||||
|
'db_table': 'rbac_user_permission',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='userpermission',
|
||||||
|
constraint=models.UniqueConstraint(fields=['user', 'permission'], name='unique_user_permission'),
|
||||||
|
),
|
||||||
|
]
|
||||||
88
api/rbac/migrations/0002_data_permissions.py
Normal file
88
api/rbac/migrations/0002_data_permissions.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Data migration que:
|
||||||
|
1. Crea el catálogo global de permisos (RolePermission).
|
||||||
|
2. Para cada Organizacion existente, crea los 5 roles por defecto con sus permisos.
|
||||||
|
3. Para cada CustomUser existente, mapea sus auth.Group actuales al UserRole equivalente.
|
||||||
|
|
||||||
|
Usa get_or_create en todos los pasos — segura de ejecutar múltiples veces.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
# Importamos solo constantes (no modelos ni funciones con imports de Django)
|
||||||
|
# para que la migration sea estable ante futuros refactors del código de la app.
|
||||||
|
from api.rbac.roles import PERMISSIONS_CATALOG, DEFAULT_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
def _crear_permisos(RolePermission):
|
||||||
|
perms_map = {}
|
||||||
|
for codename, descripcion, modulo in PERMISSIONS_CATALOG:
|
||||||
|
perm, _ = RolePermission.objects.get_or_create(
|
||||||
|
codename=codename,
|
||||||
|
defaults={'descripcion': descripcion, 'modulo': modulo},
|
||||||
|
)
|
||||||
|
perms_map[codename] = perm
|
||||||
|
return perms_map
|
||||||
|
|
||||||
|
|
||||||
|
def _crear_roles_org(OrganizationRole, org, perms_map):
|
||||||
|
for nombre, config in DEFAULT_ROLES.items():
|
||||||
|
role, created = OrganizationRole.objects.get_or_create(
|
||||||
|
organizacion=org,
|
||||||
|
nombre=nombre,
|
||||||
|
defaults={
|
||||||
|
'descripcion': config['descripcion'],
|
||||||
|
'is_admin_role': config.get('is_admin_role', False),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
role_perms = [perms_map[c] for c in config['permissions'] if c in perms_map]
|
||||||
|
role.permissions.set(role_perms)
|
||||||
|
|
||||||
|
|
||||||
|
def seed_rbac_data(apps, schema_editor):
|
||||||
|
RolePermission = apps.get_model('rbac', 'RolePermission')
|
||||||
|
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
|
||||||
|
UserRole = apps.get_model('rbac', 'UserRole')
|
||||||
|
Organizacion = apps.get_model('organization', 'Organizacion')
|
||||||
|
CustomUser = apps.get_model('cuser', 'CustomUser')
|
||||||
|
|
||||||
|
# Paso 1 — Catálogo de permisos
|
||||||
|
perms_map = _crear_permisos(RolePermission)
|
||||||
|
|
||||||
|
# Paso 2 — Roles por defecto para cada organización existente
|
||||||
|
for org in Organizacion.objects.all():
|
||||||
|
_crear_roles_org(OrganizationRole, org, perms_map)
|
||||||
|
|
||||||
|
# Paso 3 — Mapeo de usuarios: auth.Group → UserRole
|
||||||
|
# Solo usuarios que tengan organización asignada y grupos asignados
|
||||||
|
for user in CustomUser.objects.filter(organizacion__isnull=False).prefetch_related('groups'):
|
||||||
|
for group in user.groups.all():
|
||||||
|
try:
|
||||||
|
role = OrganizationRole.objects.get(
|
||||||
|
organizacion=user.organizacion,
|
||||||
|
nombre=group.name,
|
||||||
|
)
|
||||||
|
UserRole.objects.get_or_create(user=user, role=role)
|
||||||
|
except OrganizationRole.DoesNotExist:
|
||||||
|
# El grupo no tiene equivalente en los roles por defecto — se ignora
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_seed(apps, schema_editor):
|
||||||
|
# Revertir borra todos los datos RBAC. Los auth.Group originales no se tocan.
|
||||||
|
apps.get_model('rbac', 'UserRole').objects.all().delete()
|
||||||
|
apps.get_model('rbac', 'OrganizationRole').objects.all().delete()
|
||||||
|
apps.get_model('rbac', 'RolePermission').objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rbac', '0001_initial'),
|
||||||
|
('cuser', '0005_customuser_rfc_fk_to_m2m'),
|
||||||
|
('organization', '0003_organizacion_apply_auto_download'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(seed_rbac_data, reverse_code=reverse_seed),
|
||||||
|
]
|
||||||
56
api/rbac/migrations/0003_notificaciones_receive.py
Normal file
56
api/rbac/migrations/0003_notificaciones_receive.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
Agrega el permiso notificaciones.receive al catálogo y lo asigna a todos los
|
||||||
|
OrganizationRole que correspondan a los 5 roles por defecto (en todas las orgs).
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
NUEVO_PERMISO = (
|
||||||
|
'notificaciones.receive',
|
||||||
|
'Recibir notificaciones automáticas de eventos',
|
||||||
|
'notificaciones',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Todos los roles por defecto deben recibir notificaciones
|
||||||
|
ROLES_CON_PERMISO = ['admin', 'developer', 'Agente Aduanal', 'user', 'Importador']
|
||||||
|
|
||||||
|
|
||||||
|
def agregar_notificaciones_receive(apps, schema_editor):
|
||||||
|
RolePermission = apps.get_model('rbac', 'RolePermission')
|
||||||
|
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
|
||||||
|
|
||||||
|
codename, descripcion, modulo = NUEVO_PERMISO
|
||||||
|
perm, _ = RolePermission.objects.get_or_create(
|
||||||
|
codename=codename,
|
||||||
|
defaults={'descripcion': descripcion, 'modulo': modulo},
|
||||||
|
)
|
||||||
|
|
||||||
|
roles = OrganizationRole.objects.filter(nombre__in=ROLES_CON_PERMISO)
|
||||||
|
for role in roles:
|
||||||
|
role.permissions.add(perm)
|
||||||
|
|
||||||
|
|
||||||
|
def revertir(apps, schema_editor):
|
||||||
|
RolePermission = apps.get_model('rbac', 'RolePermission')
|
||||||
|
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
|
||||||
|
|
||||||
|
try:
|
||||||
|
perm = RolePermission.objects.get(codename='notificaciones.receive')
|
||||||
|
except RolePermission.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
for role in OrganizationRole.objects.all():
|
||||||
|
role.permissions.remove(perm)
|
||||||
|
|
||||||
|
perm.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rbac', '0002_data_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(agregar_notificaciones_receive, reverse_code=revertir),
|
||||||
|
]
|
||||||
57
api/rbac/migrations/0004_auditoria_permissions.py
Normal file
57
api/rbac/migrations/0004_auditoria_permissions.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
Agrega los permisos auditoria.view y auditoria.process al catálogo y los asigna
|
||||||
|
a los roles admin, developer (ambos) y Agente Aduanal (solo view).
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
NUEVOS_PERMISOS = [
|
||||||
|
('auditoria.view', 'Ver estado y resultados de auditoría VUCEM', 'auditoria'),
|
||||||
|
('auditoria.process', 'Lanzar procesos de auditoría y reauditoría', 'auditoria'),
|
||||||
|
]
|
||||||
|
|
||||||
|
ROLES_AUDITORIA_FULL = ['admin', 'developer']
|
||||||
|
ROLES_AUDITORIA_VIEW = ['Agente Aduanal']
|
||||||
|
|
||||||
|
|
||||||
|
def agregar_auditoria(apps, schema_editor):
|
||||||
|
RolePermission = apps.get_model('rbac', 'RolePermission')
|
||||||
|
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
|
||||||
|
|
||||||
|
perms = {}
|
||||||
|
for codename, descripcion, modulo in NUEVOS_PERMISOS:
|
||||||
|
perm, _ = RolePermission.objects.get_or_create(
|
||||||
|
codename=codename,
|
||||||
|
defaults={'descripcion': descripcion, 'modulo': modulo},
|
||||||
|
)
|
||||||
|
perms[codename] = perm
|
||||||
|
|
||||||
|
for role in OrganizationRole.objects.filter(nombre__in=ROLES_AUDITORIA_FULL):
|
||||||
|
role.permissions.add(perms['auditoria.view'], perms['auditoria.process'])
|
||||||
|
|
||||||
|
for role in OrganizationRole.objects.filter(nombre__in=ROLES_AUDITORIA_VIEW):
|
||||||
|
role.permissions.add(perms['auditoria.view'])
|
||||||
|
|
||||||
|
|
||||||
|
def revertir(apps, schema_editor):
|
||||||
|
RolePermission = apps.get_model('rbac', 'RolePermission')
|
||||||
|
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
|
||||||
|
|
||||||
|
for codename, _, _ in NUEVOS_PERMISOS:
|
||||||
|
try:
|
||||||
|
perm = RolePermission.objects.get(codename=codename)
|
||||||
|
except RolePermission.DoesNotExist:
|
||||||
|
continue
|
||||||
|
for role in OrganizationRole.objects.all():
|
||||||
|
role.permissions.remove(perm)
|
||||||
|
perm.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rbac', '0003_notificaciones_receive'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(agregar_auditoria, reverse_code=revertir),
|
||||||
|
]
|
||||||
0
api/rbac/migrations/__init__.py
Normal file
0
api/rbac/migrations/__init__.py
Normal file
109
api/rbac/models.py
Normal file
109
api/rbac/models.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermission(models.Model):
|
||||||
|
"""Catálogo global de permisos de la aplicación. Se define una vez y es compartido por todas las orgs."""
|
||||||
|
codename = models.CharField(max_length=100, unique=True)
|
||||||
|
descripcion = models.CharField(max_length=255)
|
||||||
|
modulo = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.codename
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'rbac_role_permission'
|
||||||
|
ordering = ['modulo', 'codename']
|
||||||
|
verbose_name = 'Permiso'
|
||||||
|
verbose_name_plural = 'Permisos'
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationRole(models.Model):
|
||||||
|
"""Rol de una organización. Cada org tiene su propio conjunto de roles con sus permisos."""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
organizacion = models.ForeignKey(
|
||||||
|
'organization.Organizacion',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='roles',
|
||||||
|
)
|
||||||
|
nombre = models.CharField(max_length=100)
|
||||||
|
descripcion = models.CharField(max_length=255, blank=True)
|
||||||
|
# El rol admin maestro no puede ser removido del owner de la org
|
||||||
|
is_admin_role = models.BooleanField(default=False)
|
||||||
|
permissions = models.ManyToManyField(
|
||||||
|
RolePermission,
|
||||||
|
blank=True,
|
||||||
|
related_name='roles',
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.nombre} ({self.organizacion})'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'rbac_organization_role'
|
||||||
|
ordering = ['nombre']
|
||||||
|
verbose_name = 'Rol de Organización'
|
||||||
|
verbose_name_plural = 'Roles de Organización'
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['organizacion', 'nombre'], name='unique_role_per_org'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UserRole(models.Model):
|
||||||
|
"""Asignación de un rol a un usuario dentro de su organización."""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='user_roles',
|
||||||
|
)
|
||||||
|
role = models.ForeignKey(
|
||||||
|
OrganizationRole,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='user_roles',
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.user} → {self.role.nombre}'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'rbac_user_role'
|
||||||
|
verbose_name = 'Rol de Usuario'
|
||||||
|
verbose_name_plural = 'Roles de Usuario'
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['user', 'role'], name='unique_user_role'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermission(models.Model):
|
||||||
|
"""Permiso singular asignado directamente a un usuario, sin necesidad de rol.
|
||||||
|
granted=True otorga, granted=False deniega explícitamente (override sobre roles)."""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='rbac_permissions',
|
||||||
|
)
|
||||||
|
permission = models.ForeignKey(
|
||||||
|
RolePermission,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='user_overrides',
|
||||||
|
)
|
||||||
|
granted = models.BooleanField(default=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
estado = 'GRANT' if self.granted else 'DENY'
|
||||||
|
return f'{estado}: {self.user} → {self.permission.codename}'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'rbac_user_permission'
|
||||||
|
verbose_name = 'Permiso Singular'
|
||||||
|
verbose_name_plural = 'Permisos Singulares'
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['user', 'permission'], name='unique_user_permission'),
|
||||||
|
]
|
||||||
176
api/rbac/roles.py
Normal file
176
api/rbac/roles.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Catálogo de permisos y configuración de roles por defecto.
|
||||||
|
# Este módulo es importado tanto por la data migration como por el signal de Organizacion,
|
||||||
|
# por lo que NO debe importar modelos directamente al nivel de módulo.
|
||||||
|
|
||||||
|
# --- CATÁLOGO DE PERMISOS ---
|
||||||
|
# (codename, descripcion, modulo)
|
||||||
|
PERMISSIONS_CATALOG = [
|
||||||
|
# Usuarios
|
||||||
|
('usuarios.view', 'Ver usuarios de la organización', 'usuarios'),
|
||||||
|
('usuarios.create', 'Crear usuarios en la organización', 'usuarios'),
|
||||||
|
('usuarios.edit', 'Modificar usuarios de la organización', 'usuarios'),
|
||||||
|
('usuarios.delete', 'Eliminar usuarios de la organización', 'usuarios'),
|
||||||
|
('usuarios.manage_roles', 'Asignar y revocar roles a usuarios', 'usuarios'),
|
||||||
|
('usuarios.change_password', 'Cambiar contraseña de otro usuario', 'usuarios'),
|
||||||
|
# Pedimentos
|
||||||
|
('pedimentos.view', 'Ver pedimentos', 'pedimentos'),
|
||||||
|
('pedimentos.create', 'Crear e importar pedimentos', 'pedimentos'),
|
||||||
|
('pedimentos.edit', 'Modificar pedimentos', 'pedimentos'),
|
||||||
|
('pedimentos.delete', 'Eliminar pedimentos', 'pedimentos'),
|
||||||
|
('pedimentos.process', 'Procesar pedimentos contra VUCEM', 'pedimentos'),
|
||||||
|
# Importadores
|
||||||
|
('importadores.view', 'Ver importadores', 'importadores'),
|
||||||
|
('importadores.create', 'Crear importadores', 'importadores'),
|
||||||
|
('importadores.edit', 'Modificar importadores', 'importadores'),
|
||||||
|
('importadores.delete', 'Eliminar importadores', 'importadores'),
|
||||||
|
# Partidas
|
||||||
|
('partidas.view', 'Ver partidas', 'partidas'),
|
||||||
|
('partidas.create', 'Crear partidas', 'partidas'),
|
||||||
|
('partidas.edit', 'Modificar partidas', 'partidas'),
|
||||||
|
('partidas.delete', 'Eliminar partidas', 'partidas'),
|
||||||
|
# Remesas
|
||||||
|
('remesas.view', 'Ver remesas', 'remesas'),
|
||||||
|
# COVEs
|
||||||
|
('coves.view', 'Ver COVEs', 'coves'),
|
||||||
|
('coves.create', 'Crear COVEs', 'coves'),
|
||||||
|
('coves.edit', 'Modificar COVEs', 'coves'),
|
||||||
|
('coves.delete', 'Eliminar COVEs', 'coves'),
|
||||||
|
# E-Documents
|
||||||
|
('edocuments.view', 'Ver E-Documents', 'edocuments'),
|
||||||
|
('edocuments.create', 'Crear E-Documents', 'edocuments'),
|
||||||
|
('edocuments.edit', 'Modificar E-Documents', 'edocuments'),
|
||||||
|
('edocuments.delete', 'Eliminar E-Documents', 'edocuments'),
|
||||||
|
# Acuses
|
||||||
|
('acuses.view', 'Ver acuses', 'acuses'),
|
||||||
|
# Documentos (expediente)
|
||||||
|
('documentos.view', 'Ver documentos del expediente', 'documentos'),
|
||||||
|
('documentos.upload', 'Cargar documentos', 'documentos'),
|
||||||
|
('documentos.download', 'Descargar documentos y ZIPs', 'documentos'),
|
||||||
|
('documentos.delete', 'Eliminar documentos del expediente', 'documentos'),
|
||||||
|
# VUCEM
|
||||||
|
('vucem.view', 'Ver credenciales VUCEM', 'vucem'),
|
||||||
|
('vucem.manage', 'Gestionar credenciales VUCEM', 'vucem'),
|
||||||
|
# Reportes
|
||||||
|
('reportes.view', 'Ver reportes y dashboard', 'reportes'),
|
||||||
|
('reportes.export', 'Exportar reportes a CSV/Excel', 'reportes'),
|
||||||
|
# DataStage
|
||||||
|
('datastage.view', 'Ver DataStages', 'datastage'),
|
||||||
|
('datastage.create', 'Crear DataStages', 'datastage'),
|
||||||
|
('datastage.process', 'Procesar DataStages', 'datastage'),
|
||||||
|
('datastage.delete', 'Eliminar DataStages', 'datastage'),
|
||||||
|
# Organización
|
||||||
|
('organizacion.view', 'Ver datos de la organización', 'organizacion'),
|
||||||
|
('organizacion.edit', 'Modificar datos de la organización', 'organizacion'),
|
||||||
|
# Notificaciones
|
||||||
|
('notificaciones.view', 'Ver notificaciones propias', 'notificaciones'),
|
||||||
|
('notificaciones.receive', 'Recibir notificaciones automáticas de eventos', 'notificaciones'),
|
||||||
|
# Cards / Analytics
|
||||||
|
('cards.view', 'Ver dashboard y analytics', 'cards'),
|
||||||
|
# Auditoría
|
||||||
|
('auditoria.view', 'Ver estado y resultados de auditoría VUCEM', 'auditoria'),
|
||||||
|
('auditoria.process', 'Lanzar procesos de auditoría y reauditoría', 'auditoria'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Conjuntos reutilizables para armar la matriz de permisos por rol
|
||||||
|
_IMPORTADORES_FULL = ['importadores.view', 'importadores.create', 'importadores.edit', 'importadores.delete']
|
||||||
|
_PEDIMENTOS_FULL = ['pedimentos.view', 'pedimentos.create', 'pedimentos.edit', 'pedimentos.delete', 'pedimentos.process']
|
||||||
|
_PARTIDAS_FULL = ['partidas.view', 'partidas.create', 'partidas.edit', 'partidas.delete']
|
||||||
|
_COVES_FULL = ['coves.view', 'coves.create', 'coves.edit', 'coves.delete']
|
||||||
|
_EDOCUMENTS_FULL = ['edocuments.view', 'edocuments.create', 'edocuments.edit', 'edocuments.delete']
|
||||||
|
_DOCUMENTOS_FULL = ['documentos.view', 'documentos.upload', 'documentos.download', 'documentos.delete']
|
||||||
|
_VUCEM_FULL = ['vucem.view', 'vucem.manage']
|
||||||
|
_REPORTES_FULL = ['reportes.view', 'reportes.export']
|
||||||
|
_DATASTAGE_FULL = ['datastage.view', 'datastage.create', 'datastage.process']
|
||||||
|
|
||||||
|
# --- ROLES POR DEFECTO ---
|
||||||
|
# Cada entrada: nombre → { descripcion, is_admin_role, permissions }
|
||||||
|
DEFAULT_ROLES = {
|
||||||
|
'admin': {
|
||||||
|
'descripcion': 'Administrador de la organización',
|
||||||
|
'is_admin_role': True,
|
||||||
|
'permissions': [
|
||||||
|
'usuarios.view', 'usuarios.create', 'usuarios.edit', 'usuarios.delete',
|
||||||
|
'usuarios.manage_roles', 'usuarios.change_password',
|
||||||
|
*_PEDIMENTOS_FULL, *_PARTIDAS_FULL, 'remesas.view',
|
||||||
|
*_COVES_FULL, *_EDOCUMENTS_FULL, 'acuses.view',
|
||||||
|
*_DOCUMENTOS_FULL, *_VUCEM_FULL,
|
||||||
|
*_IMPORTADORES_FULL,
|
||||||
|
*_REPORTES_FULL, *_DATASTAGE_FULL,
|
||||||
|
'organizacion.view', 'organizacion.edit',
|
||||||
|
'notificaciones.view', 'notificaciones.receive', 'cards.view',
|
||||||
|
'auditoria.view', 'auditoria.process',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'developer': {
|
||||||
|
'descripcion': 'Desarrollador con acceso técnico avanzado',
|
||||||
|
'is_admin_role': False,
|
||||||
|
'permissions': [
|
||||||
|
'usuarios.view', 'usuarios.create',
|
||||||
|
*_PEDIMENTOS_FULL, *_PARTIDAS_FULL, 'remesas.view',
|
||||||
|
*_COVES_FULL, *_EDOCUMENTS_FULL, 'acuses.view',
|
||||||
|
*_DOCUMENTOS_FULL, *_VUCEM_FULL, *_IMPORTADORES_FULL,
|
||||||
|
*_REPORTES_FULL, *_DATASTAGE_FULL,
|
||||||
|
'organizacion.view',
|
||||||
|
'notificaciones.view', 'notificaciones.receive', 'cards.view',
|
||||||
|
'auditoria.view', 'auditoria.process',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Agente Aduanal': {
|
||||||
|
'descripcion': 'Agente aduanal operativo',
|
||||||
|
'is_admin_role': False,
|
||||||
|
'permissions': [
|
||||||
|
*_PEDIMENTOS_FULL, *_PARTIDAS_FULL, 'remesas.view',
|
||||||
|
*_COVES_FULL, *_EDOCUMENTS_FULL, 'acuses.view',
|
||||||
|
*_DOCUMENTOS_FULL, *_VUCEM_FULL,
|
||||||
|
*_REPORTES_FULL,
|
||||||
|
'organizacion.view',
|
||||||
|
'notificaciones.view', 'notificaciones.receive', 'cards.view',
|
||||||
|
'auditoria.view',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'descripcion': 'Usuario básico de la organización',
|
||||||
|
'is_admin_role': False,
|
||||||
|
'permissions': [
|
||||||
|
'pedimentos.view', 'pedimentos.process',
|
||||||
|
'partidas.view', 'remesas.view',
|
||||||
|
'coves.view', 'edocuments.view', 'acuses.view',
|
||||||
|
'documentos.view', 'documentos.upload', 'documentos.download',
|
||||||
|
'reportes.view', 'datastage.view',
|
||||||
|
'notificaciones.view', 'notificaciones.receive', 'cards.view',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Importador': {
|
||||||
|
'descripcion': 'Importador con acceso filtrado por RFC',
|
||||||
|
'is_admin_role': False,
|
||||||
|
'permissions': [
|
||||||
|
'pedimentos.view', 'partidas.view', 'remesas.view',
|
||||||
|
'coves.view', 'edocuments.view', 'acuses.view',
|
||||||
|
'documentos.view', 'documentos.download',
|
||||||
|
'vucem.view', 'vucem.manage',
|
||||||
|
'reportes.view',
|
||||||
|
'notificaciones.view', 'notificaciones.receive', 'cards.view',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def crear_roles_para_organizacion(organizacion):
|
||||||
|
"""Crea los 5 roles por defecto para una organización, con sus permisos.
|
||||||
|
Usa get_or_create — seguro de ejecutar múltiples veces."""
|
||||||
|
from api.rbac.models import RolePermission, OrganizationRole
|
||||||
|
|
||||||
|
perms_map = {p.codename: p for p in RolePermission.objects.all()}
|
||||||
|
|
||||||
|
for nombre, config in DEFAULT_ROLES.items():
|
||||||
|
role, created = OrganizationRole.objects.get_or_create(
|
||||||
|
organizacion=organizacion,
|
||||||
|
nombre=nombre,
|
||||||
|
defaults={
|
||||||
|
'descripcion': config['descripcion'],
|
||||||
|
'is_admin_role': config.get('is_admin_role', False),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
role_perms = [perms_map[c] for c in config['permissions'] if c in perms_map]
|
||||||
|
role.permissions.set(role_perms)
|
||||||
105
api/rbac/serializers.py
Normal file
105
api/rbac/serializers.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from api.rbac.models import OrganizationRole, RolePermission, UserPermission, UserRole
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermissionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RolePermission
|
||||||
|
fields = ['id', 'codename', 'descripcion', 'modulo']
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationRoleSerializer(serializers.ModelSerializer):
|
||||||
|
permissions = RolePermissionSerializer(many=True, read_only=True)
|
||||||
|
permission_ids = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=RolePermission.objects.all(),
|
||||||
|
many=True,
|
||||||
|
write_only=True,
|
||||||
|
source='permissions',
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
user_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrganizationRole
|
||||||
|
fields = [
|
||||||
|
'id', 'nombre', 'descripcion', 'is_admin_role',
|
||||||
|
'permissions', 'permission_ids', 'user_count',
|
||||||
|
'created_at', 'updated_at',
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'is_admin_role', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationRoleWriteSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer para crear/editar roles — recibe lista de IDs de permisos."""
|
||||||
|
permission_ids = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=RolePermission.objects.all(),
|
||||||
|
many=True,
|
||||||
|
source='permissions',
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrganizationRole
|
||||||
|
fields = ['nombre', 'descripcion', 'permission_ids']
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
perms = validated_data.pop('permissions', [])
|
||||||
|
role = OrganizationRole.objects.create(**validated_data)
|
||||||
|
role.permissions.set(perms)
|
||||||
|
return role
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
perms = validated_data.pop('permissions', None)
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
setattr(instance, attr, value)
|
||||||
|
instance.save()
|
||||||
|
if perms is not None:
|
||||||
|
instance.permissions.set(perms)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class _UserMinimalSerializer(serializers.Serializer):
|
||||||
|
id = serializers.UUIDField()
|
||||||
|
username = serializers.CharField()
|
||||||
|
email = serializers.EmailField()
|
||||||
|
first_name = serializers.CharField()
|
||||||
|
last_name = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class _RoleMinimalSerializer(serializers.Serializer):
|
||||||
|
id = serializers.UUIDField()
|
||||||
|
nombre = serializers.CharField()
|
||||||
|
descripcion = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class UserRoleSerializer(serializers.ModelSerializer):
|
||||||
|
user = _UserMinimalSerializer(read_only=True)
|
||||||
|
role = _RoleMinimalSerializer(read_only=True)
|
||||||
|
# write
|
||||||
|
user_id = serializers.UUIDField(write_only=True, source='user')
|
||||||
|
role_id = serializers.UUIDField(write_only=True, source='role')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserRole
|
||||||
|
fields = ['id', 'user', 'user_id', 'role', 'role_id', 'created_at']
|
||||||
|
read_only_fields = ['id', 'created_at']
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermissionSerializer(serializers.ModelSerializer):
|
||||||
|
user = _UserMinimalSerializer(read_only=True)
|
||||||
|
permission = RolePermissionSerializer(read_only=True)
|
||||||
|
# write
|
||||||
|
user_id = serializers.UUIDField(write_only=True, source='user')
|
||||||
|
permission_id = serializers.IntegerField(write_only=True, source='permission')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserPermission
|
||||||
|
fields = ['id', 'user', 'user_id', 'permission', 'permission_id', 'granted', 'created_at']
|
||||||
|
read_only_fields = ['id', 'created_at']
|
||||||
|
|
||||||
|
|
||||||
|
class MyPermissionsSerializer(serializers.Serializer):
|
||||||
|
"""Respuesta de /rbac/my-permissions/ — permisos efectivos del usuario autenticado."""
|
||||||
|
permissions = serializers.ListField(child=serializers.CharField())
|
||||||
|
roles = serializers.ListField(child=serializers.CharField())
|
||||||
23
api/rbac/urls.py
Normal file
23
api/rbac/urls.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from api.rbac.views import (
|
||||||
|
MyPermissionsView,
|
||||||
|
OrganizationRoleViewSet,
|
||||||
|
RolePermissionViewSet,
|
||||||
|
SwitchOrganizationView,
|
||||||
|
UserPermissionViewSet,
|
||||||
|
UserRoleViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'permissions', RolePermissionViewSet, basename='rbac-permission')
|
||||||
|
router.register(r'roles', OrganizationRoleViewSet, basename='rbac-role')
|
||||||
|
router.register(r'user-roles', UserRoleViewSet, basename='rbac-user-role')
|
||||||
|
router.register(r'user-permissions', UserPermissionViewSet, basename='rbac-user-permission')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls)),
|
||||||
|
path('my-permissions/', MyPermissionsView.as_view(), name='rbac-my-permissions'),
|
||||||
|
path('switch-organization/', SwitchOrganizationView.as_view(), name='rbac-switch-org'),
|
||||||
|
]
|
||||||
412
api/rbac/views.py
Normal file
412
api/rbac/views.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
from django.db.models import Count
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
from api.rbac.models import OrganizationRole, RolePermission, UserPermission, UserRole
|
||||||
|
from api.rbac.serializers import (
|
||||||
|
MyPermissionsSerializer,
|
||||||
|
OrganizationRoleSerializer,
|
||||||
|
OrganizationRoleWriteSerializer,
|
||||||
|
RolePermissionSerializer,
|
||||||
|
UserPermissionSerializer,
|
||||||
|
UserRoleSerializer,
|
||||||
|
)
|
||||||
|
from core.permissions import OrgScopedPermission, get_org_context, is_internal_service_request, require_permission, user_has_permission
|
||||||
|
|
||||||
|
|
||||||
|
def _require_manage_roles(user):
|
||||||
|
"""Retorna True si el usuario puede gestionar roles/permisos en su org."""
|
||||||
|
return user.is_superuser or user_has_permission(user, 'usuarios.manage_roles')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Catálogo de permisos (lectura para todos los autenticados con org)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RolePermissionViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""Lista el catálogo global de permisos disponibles, agrupados por módulo."""
|
||||||
|
my_tags = ['RBAC']
|
||||||
|
serializer_class = RolePermissionSerializer
|
||||||
|
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return RolePermission.objects.all().order_by('modulo', 'codename')
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='by-module')
|
||||||
|
def by_module(self, request):
|
||||||
|
"""Devuelve el catálogo agrupado por módulo."""
|
||||||
|
perms = self.get_queryset()
|
||||||
|
result = {}
|
||||||
|
for p in perms:
|
||||||
|
result.setdefault(p.modulo, []).append(
|
||||||
|
RolePermissionSerializer(p).data
|
||||||
|
)
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Roles de la organización
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class OrganizationRoleViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
CRUD de roles de la organización activa.
|
||||||
|
Solo usuarios con usuarios.manage_roles pueden crear/editar/eliminar.
|
||||||
|
"""
|
||||||
|
my_tags = ['RBAC']
|
||||||
|
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
return (
|
||||||
|
OrganizationRole.objects
|
||||||
|
.annotate(user_count=Count('user_roles'))
|
||||||
|
.prefetch_related('permissions')
|
||||||
|
.order_by('nombre')
|
||||||
|
)
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
return OrganizationRole.objects.none()
|
||||||
|
return (
|
||||||
|
OrganizationRole.objects
|
||||||
|
.filter(organizacion=org)
|
||||||
|
.annotate(user_count=Count('user_roles'))
|
||||||
|
.prefetch_related('permissions')
|
||||||
|
.order_by('nombre')
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action in ('create', 'update', 'partial_update'):
|
||||||
|
return OrganizationRoleWriteSerializer
|
||||||
|
return OrganizationRoleSerializer
|
||||||
|
|
||||||
|
def _check_manage_roles(self):
|
||||||
|
if not _require_manage_roles(self.request.user):
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
err = self._check_manage_roles()
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
org = get_org_context(request.user)
|
||||||
|
if not org:
|
||||||
|
return Response({'detail': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save(organizacion=org)
|
||||||
|
return Response(
|
||||||
|
OrganizationRoleSerializer(serializer.instance).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
err = self._check_manage_roles()
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
instance = self.get_object()
|
||||||
|
# No se puede cambiar nombre ni permisos de un rol is_admin_role
|
||||||
|
if instance.is_admin_role and not request.user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'No se puede modificar un rol de administrador.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
err = self._check_manage_roles()
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
instance = self.get_object()
|
||||||
|
if instance.is_admin_role and not request.user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'No se puede eliminar un rol de administrador.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if instance.user_roles.exists():
|
||||||
|
return Response(
|
||||||
|
{'detail': 'No se puede eliminar un rol con usuarios asignados.'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Asignación de roles a usuarios
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UserRoleViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
Asigna y revoca roles de usuarios en la organización activa.
|
||||||
|
Solo usuarios con usuarios.manage_roles pueden modificar.
|
||||||
|
"""
|
||||||
|
my_tags = ['RBAC']
|
||||||
|
serializer_class = UserRoleSerializer
|
||||||
|
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
|
||||||
|
http_method_names = ['get', 'post', 'delete', 'head', 'options']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
qs = UserRole.objects.select_related('user', 'role')
|
||||||
|
user_id = self.request.query_params.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
qs = qs.filter(user_id=user_id)
|
||||||
|
return qs
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
return UserRole.objects.none()
|
||||||
|
qs = (
|
||||||
|
UserRole.objects
|
||||||
|
.filter(role__organizacion=org)
|
||||||
|
.select_related('user', 'role')
|
||||||
|
)
|
||||||
|
user_id = self.request.query_params.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
qs = qs.filter(user_id=user_id)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
if not _require_manage_roles(request.user):
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
org = get_org_context(request.user)
|
||||||
|
if not org:
|
||||||
|
return Response({'detail': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
user_id = request.data.get('user_id')
|
||||||
|
role_id = request.data.get('role_id')
|
||||||
|
|
||||||
|
if not user_id or not role_id:
|
||||||
|
return Response({'detail': 'user_id y role_id son requeridos.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Verificar que el rol pertenece a la misma org
|
||||||
|
try:
|
||||||
|
role = OrganizationRole.objects.get(id=role_id, organizacion=org)
|
||||||
|
except OrganizationRole.DoesNotExist:
|
||||||
|
return Response({'detail': 'El rol no pertenece a esta organización.'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# Verificar que el usuario pertenece a la misma org
|
||||||
|
from api.cuser.models import CustomUser
|
||||||
|
try:
|
||||||
|
target_user = CustomUser.objects.get(id=user_id, organizacion=org)
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
return Response({'detail': 'El usuario no pertenece a esta organización.'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
user_role, created = UserRole.objects.get_or_create(user=target_user, role=role)
|
||||||
|
serializer = self.get_serializer(user_role)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
if not _require_manage_roles(request.user):
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
instance = self.get_object()
|
||||||
|
org = get_org_context(request.user)
|
||||||
|
|
||||||
|
# Proteger al owner de la org: no se le puede quitar el rol admin
|
||||||
|
if org and hasattr(org, 'owner') and org.owner and instance.user == org.owner:
|
||||||
|
if instance.role.is_admin_role:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'No se puede revocar el rol de administrador al propietario de la organización.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Permisos singulares (overrides por usuario)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UserPermissionViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
Otorga o deniega permisos singulares a usuarios, sin necesidad de crear un rol.
|
||||||
|
granted=true → otorgar; granted=false → denegar explícitamente (override sobre roles).
|
||||||
|
Solo usuarios con usuarios.manage_roles pueden modificar.
|
||||||
|
"""
|
||||||
|
my_tags = ['RBAC']
|
||||||
|
serializer_class = UserPermissionSerializer
|
||||||
|
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
|
||||||
|
http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
|
qs = UserPermission.objects.select_related('user', 'permission')
|
||||||
|
user_id = self.request.query_params.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
qs = qs.filter(user_id=user_id)
|
||||||
|
return qs
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
return UserPermission.objects.none()
|
||||||
|
qs = (
|
||||||
|
UserPermission.objects
|
||||||
|
.filter(user__organizacion=org)
|
||||||
|
.select_related('user', 'permission')
|
||||||
|
)
|
||||||
|
user_id = self.request.query_params.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
qs = qs.filter(user_id=user_id)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def _check(self):
|
||||||
|
if not _require_manage_roles(self.request.user):
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
err = self._check()
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
org = get_org_context(request.user)
|
||||||
|
if not org:
|
||||||
|
return Response({'detail': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
user_id = request.data.get('user_id')
|
||||||
|
permission_id = request.data.get('permission_id')
|
||||||
|
granted = request.data.get('granted', True)
|
||||||
|
|
||||||
|
if not user_id or not permission_id:
|
||||||
|
return Response({'detail': 'user_id y permission_id son requeridos.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
from api.cuser.models import CustomUser
|
||||||
|
try:
|
||||||
|
target_user = CustomUser.objects.get(id=user_id, organizacion=org)
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
return Response({'detail': 'El usuario no pertenece a esta organización.'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
try:
|
||||||
|
perm = RolePermission.objects.get(id=permission_id)
|
||||||
|
except RolePermission.DoesNotExist:
|
||||||
|
return Response({'detail': 'Permiso no encontrado.'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
override, created = UserPermission.objects.update_or_create(
|
||||||
|
user=target_user,
|
||||||
|
permission=perm,
|
||||||
|
defaults={'granted': granted},
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(override)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
err = self._check()
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
return super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
err = self._check()
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Mis permisos efectivos (para el frontend)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MyPermissionsView(APIView):
|
||||||
|
"""
|
||||||
|
Retorna los permisos efectivos del usuario autenticado.
|
||||||
|
El frontend usa esto para decidir qué mostrar/ocultar.
|
||||||
|
"""
|
||||||
|
my_tags = ['RBAC']
|
||||||
|
permission_classes = [IsAuthenticated & OrgScopedPermission]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
user = request.user
|
||||||
|
org = get_org_context(user)
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
all_perms = list(RolePermission.objects.values_list('codename', flat=True))
|
||||||
|
return Response({'permissions': all_perms, 'roles': ['superuser']})
|
||||||
|
|
||||||
|
if not org:
|
||||||
|
return Response({'permissions': [], 'roles': []})
|
||||||
|
|
||||||
|
# Roles del usuario en la org
|
||||||
|
roles = list(
|
||||||
|
UserRole.objects.filter(user=user, role__organizacion=org)
|
||||||
|
.values_list('role__nombre', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Permisos de roles
|
||||||
|
perms_set = set(
|
||||||
|
UserRole.objects.filter(user=user, role__organizacion=org)
|
||||||
|
.values_list('role__permissions__codename', flat=True)
|
||||||
|
)
|
||||||
|
perms_set.discard(None)
|
||||||
|
|
||||||
|
# Aplicar overrides singulares
|
||||||
|
for override in UserPermission.objects.filter(user=user).select_related('permission'):
|
||||||
|
if override.granted:
|
||||||
|
perms_set.add(override.permission.codename)
|
||||||
|
else:
|
||||||
|
perms_set.discard(override.permission.codename)
|
||||||
|
|
||||||
|
return Response({'permissions': sorted(perms_set), 'roles': roles})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Switch de organización (solo superusuarios)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class SwitchOrganizationView(APIView):
|
||||||
|
"""
|
||||||
|
Permite a un superusuario cambiar su organización activa.
|
||||||
|
POST { "organization_id": "<uuid>" } → actualiza active_organization del superuser.
|
||||||
|
DELETE → limpia active_organization (el superuser queda sin contexto de org).
|
||||||
|
"""
|
||||||
|
my_tags = ['RBAC']
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Solo superusuarios pueden cambiar de organización.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
org_id = request.data.get('organization_id')
|
||||||
|
if not org_id:
|
||||||
|
return Response({'detail': 'organization_id es requerido.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
from api.organization.models import Organizacion
|
||||||
|
try:
|
||||||
|
import uuid as _uuid
|
||||||
|
org = Organizacion.objects.get(id=_uuid.UUID(str(org_id)))
|
||||||
|
except (Organizacion.DoesNotExist, ValueError):
|
||||||
|
return Response({'detail': 'Organización no encontrada.'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
request.user.active_organization = org
|
||||||
|
request.user.save(update_fields=['active_organization'])
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'detail': f'Organización activa actualizada a: {org.nombre}',
|
||||||
|
'organization': {'id': str(org.id), 'nombre': org.nombre},
|
||||||
|
})
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Solo superusuarios pueden limpiar la organización activa.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
request.user.active_organization = None
|
||||||
|
request.user.save(update_fields=['active_organization'])
|
||||||
|
return Response({'detail': 'Organización activa removida.'})
|
||||||
@@ -26,11 +26,13 @@ from django.utils import timezone
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from api.utils.storage_service import storage_service
|
from api.utils.storage_service import storage_service
|
||||||
|
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
from core.permissions import (
|
from core.permissions import (
|
||||||
IsSameOrganization,
|
get_org_context,
|
||||||
IsSameOrganizationDeveloper,
|
require_permission,
|
||||||
IsSameOrganizationAndAdmin,
|
user_has_permission,
|
||||||
IsSuperUser
|
IsInternalService,
|
||||||
)
|
)
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -142,21 +144,47 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
"""
|
"""
|
||||||
ViewSet for Document model.
|
ViewSet for Document model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
|
|
||||||
model = Document
|
model = Document
|
||||||
|
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
serializer_class = DocumentSerializer
|
serializer_class = DocumentSerializer
|
||||||
# Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado)
|
|
||||||
filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento', 'created_at']
|
filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento', 'created_at']
|
||||||
# filterset_fields = ['extension', 'size', 'pedimento', 'pedimento__pedimento']
|
|
||||||
|
|
||||||
# Puedes filtrar por pedimento usando: /api/record/documents/?pedimento=<id> o /api/record/documents/?pedimento__pedimento=<numero>
|
|
||||||
# Ejemplo: /api/record/documents/?pedimento_numero=12345678
|
|
||||||
my_tags = ['Documents']
|
my_tags = ['Documents']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
# Service account (Token + superuser): acceso directo sin RBAC de org
|
||||||
|
if (self.request.user.is_authenticated and self.request.user.is_superuser and
|
||||||
|
isinstance(getattr(self.request, 'successful_authenticator', None), TokenAuthentication)):
|
||||||
|
return [IsAuthenticated(), IsInternalService()]
|
||||||
|
perms = {
|
||||||
|
'list': 'documentos.view',
|
||||||
|
'retrieve': 'documentos.view',
|
||||||
|
'create': 'documentos.upload',
|
||||||
|
'update': 'documentos.upload',
|
||||||
|
'partial_update': 'documentos.upload',
|
||||||
|
'destroy': 'documentos.delete',
|
||||||
|
'vu_documentos_errores': 'documentos.view',
|
||||||
|
'bulk_delete': 'documentos.delete',
|
||||||
|
'bulk_delete_partidas_vu': 'documentos.delete',
|
||||||
|
'bulk_delete_coves_vu': 'documentos.delete',
|
||||||
|
'bulk_delete_edocs_vu': 'documentos.delete',
|
||||||
|
'bulk_upload': 'documentos.upload',
|
||||||
|
'bulk_upload_vu': 'documentos.upload',
|
||||||
|
'create_vu_record': 'documentos.upload',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'documentos.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = self.get_queryset_filtrado_por_organizacion()
|
user = self.request.user
|
||||||
|
if user.is_superuser and isinstance(
|
||||||
|
getattr(self.request, 'successful_authenticator', None), TokenAuthentication
|
||||||
|
):
|
||||||
|
queryset = Document.objects.all()
|
||||||
|
else:
|
||||||
|
if not user_has_permission(user, 'documentos.view'):
|
||||||
|
return Document.objects.none()
|
||||||
|
queryset = self.get_queryset_filtrado_por_organizacion()
|
||||||
modulo_efc = self.request.query_params.get('modulo')
|
modulo_efc = self.request.query_params.get('modulo')
|
||||||
if modulo_efc:
|
if modulo_efc:
|
||||||
if modulo_efc == 'expedientes-detalle-pedimentos':
|
if modulo_efc == 'expedientes-detalle-pedimentos':
|
||||||
@@ -2017,7 +2045,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
|
|
||||||
|
|
||||||
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
|
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
|
||||||
serializer_class = DocumentSerializer
|
serializer_class = DocumentSerializer
|
||||||
model = Document
|
model = Document
|
||||||
my_tags = ['Documents']
|
my_tags = ['Documents']
|
||||||
@@ -2030,17 +2058,14 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
|
|||||||
import os
|
import os
|
||||||
from api.utils.storage_service import storage_service
|
from api.utils.storage_service import storage_service
|
||||||
|
|
||||||
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
|
|
||||||
raise Http404("Usuario no autenticado")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
doc = Document.objects.get(pk=pk)
|
doc = Document.objects.get(pk=pk)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
raise Http404("Documento no encontrado")
|
raise Http404("Documento no encontrado")
|
||||||
|
|
||||||
if not request.user.is_superuser:
|
org = get_org_context(request.user)
|
||||||
if doc.organizacion != request.user.organizacion:
|
if doc.organizacion != org:
|
||||||
raise Http404("No autorizado")
|
raise Http404("No autorizado")
|
||||||
|
|
||||||
if not doc.archivo:
|
if not doc.archivo:
|
||||||
raise Http404("Documento sin archivo asociado")
|
raise Http404("Documento sin archivo asociado")
|
||||||
@@ -2064,7 +2089,7 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
class BulkDownloadZipView(APIView):
|
class BulkDownloadZipView(APIView):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
|
||||||
my_tags = ['Documents']
|
my_tags = ['Documents']
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -2172,7 +2197,7 @@ class BulkDownloadZipView(APIView):
|
|||||||
logger.warning(f"No se pudo eliminar archivo temporal {tmp_path}: {e}")
|
logger.warning(f"No se pudo eliminar archivo temporal {tmp_path}: {e}")
|
||||||
|
|
||||||
class GetFuenteView(APIView):
|
class GetFuenteView(APIView):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('documentos.view')]
|
||||||
serializer_class = FuenteSerializer
|
serializer_class = FuenteSerializer
|
||||||
my_tags = ['Fuente Documentos']
|
my_tags = ['Fuente Documentos']
|
||||||
|
|
||||||
@@ -2187,7 +2212,7 @@ class GetFuenteView(APIView):
|
|||||||
return Response(serializer.data, status=200)
|
return Response(serializer.data, status=200)
|
||||||
|
|
||||||
class DocumentTypeView(APIView):
|
class DocumentTypeView(APIView):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('documentos.view')]
|
||||||
serializer_class = DocumentTypeSerializer
|
serializer_class = DocumentTypeSerializer
|
||||||
my_tags = ['Tipo de Documentos']
|
my_tags = ['Tipo de Documentos']
|
||||||
|
|
||||||
@@ -2204,7 +2229,7 @@ class DocumentTypeView(APIView):
|
|||||||
return Response(serializer.data, status=200)
|
return Response(serializer.data, status=200)
|
||||||
|
|
||||||
class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin):
|
class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
|
||||||
my_tags = ['Documents']
|
my_tags = ['Documents']
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -2306,7 +2331,7 @@ class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin):
|
|||||||
logger.warning(f"No se pudo eliminar archivo temporal {tmp_path}: {e}")
|
logger.warning(f"No se pudo eliminar archivo temporal {tmp_path}: {e}")
|
||||||
|
|
||||||
class MultiPedimentoZipDownloadView(APIView):
|
class MultiPedimentoZipDownloadView(APIView):
|
||||||
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper)]
|
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
|
||||||
my_tags = ['Documents']
|
my_tags = ['Documents']
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -2375,7 +2400,7 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
"""
|
"""
|
||||||
ViewSet for Document model.
|
ViewSet for Document model.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
|
permission_classes = [IsAuthenticated, require_permission('documentos.view')]
|
||||||
model = Document
|
model = Document
|
||||||
|
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomPagination
|
||||||
@@ -2389,6 +2414,8 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
my_tags = ['Documents']
|
my_tags = ['Documents']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if not user_has_permission(self.request.user, 'documentos.view'):
|
||||||
|
return Document.objects.none()
|
||||||
queryset = self.get_queryset_filtrado_por_organizacion()
|
queryset = self.get_queryset_filtrado_por_organizacion()
|
||||||
pedimento_id = self.request.query_params.get('pedimento')
|
pedimento_id = self.request.query_params.get('pedimento')
|
||||||
|
|
||||||
@@ -2435,8 +2462,7 @@ class TriggerPedimentoCompletoView(APIView):
|
|||||||
en el microservicio FastAPI. Reenvía el payload tal cual y devuelve
|
en el microservicio FastAPI. Reenvía el payload tal cual y devuelve
|
||||||
la respuesta del microservicio (normalmente un `task_id`).
|
la respuesta del microservicio (normalmente un `task_id`).
|
||||||
"""
|
"""
|
||||||
# permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, require_permission('pedimentos.process')]
|
||||||
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
|
|
||||||
|
|
||||||
my_tags = ['Microservice - Pedimento Completo']
|
my_tags = ['Microservice - Pedimento Completo']
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,31 @@
|
|||||||
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
|
|
||||||
from api.record.models import Document
|
|
||||||
from api.organization.models import Organizacion
|
|
||||||
from django.db.models import Count, Q
|
|
||||||
|
|
||||||
# Registrar endpoint en urls.py:
|
|
||||||
# path('dashboard/summary/', dashboard_summary)
|
|
||||||
import csv
|
import csv
|
||||||
import io
|
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 .serializers import ExportModelSerializer
|
|
||||||
from core.permissions import (
|
|
||||||
IsSameOrganization,
|
|
||||||
IsSameOrganizationDeveloper,
|
|
||||||
IsSameOrganizationAndAdmin,
|
|
||||||
IsSuperUser
|
|
||||||
)
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
import csv
|
|
||||||
import io
|
|
||||||
import openpyxl
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.apps import apps
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
|
||||||
from drf_yasg import openapi
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from core.permissions import (
|
|
||||||
IsSameOrganization,
|
|
||||||
IsSameOrganizationDeveloper,
|
|
||||||
IsSameOrganizationAndAdmin,
|
|
||||||
IsSuperUser
|
|
||||||
)
|
|
||||||
from .serializers import ExportModelSerializer
|
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from api.customs.models import Cove, EDocument, Partida, Pedimento
|
||||||
|
from api.organization.models import Organizacion
|
||||||
|
from api.record.models import Document
|
||||||
|
from core.permissions import (
|
||||||
|
get_org_context,
|
||||||
|
require_permission,
|
||||||
|
user_has_permission,
|
||||||
|
)
|
||||||
|
from .serializers import ExportModelSerializer
|
||||||
|
|
||||||
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
|
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
|
||||||
model = apps.get_model(module, model_name)
|
model = apps.get_model(module, model_name)
|
||||||
@@ -110,7 +84,11 @@ def export_model_to_excel(request, model_name, fields, module='datastage', filte
|
|||||||
|
|
||||||
class ExportDataStageView(APIView):
|
class ExportDataStageView(APIView):
|
||||||
my_tags = ['Reportes-DataStage']
|
my_tags = ['Reportes-DataStage']
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return [IsAuthenticated(), require_permission('reportes.view')()]
|
||||||
|
return [IsAuthenticated(), require_permission('reportes.export')()]
|
||||||
|
|
||||||
# Constantes para partición
|
# Constantes para partición
|
||||||
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
|
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
|
||||||
@@ -136,20 +114,14 @@ class ExportDataStageView(APIView):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Retorna RFCs distintos de Registro501 para la organización indicada. El parámetro organizacion es obligatorio."""
|
"""Retorna RFCs distintos de Registro501 para la organización activa del usuario."""
|
||||||
try:
|
try:
|
||||||
Registro501 = apps.get_model('datastage', 'Registro501')
|
Registro501 = apps.get_model('datastage', 'Registro501')
|
||||||
|
|
||||||
if not request.user.is_superuser:
|
org = get_org_context(request.user)
|
||||||
qs = Registro501.objects.filter(organizacion=request.user.organizacion)
|
if not org:
|
||||||
else:
|
return Response({'error': 'Sin organización activa'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
org_id = request.query_params.get('organizacion')
|
qs = Registro501.objects.filter(organizacion=org)
|
||||||
if not org_id:
|
|
||||||
return Response({'error': 'El parámetro organizacion es obligatorio'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
try:
|
|
||||||
qs = Registro501.objects.filter(organizacion_id=uuid.UUID(org_id))
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
return Response({'error': 'UUID de organización inválido'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
rfcs = (
|
rfcs = (
|
||||||
qs.exclude(rfc__isnull=True)
|
qs.exclude(rfc__isnull=True)
|
||||||
@@ -178,23 +150,19 @@ class ExportDataStageView(APIView):
|
|||||||
def _resolve_org_filter(self, global_filters, user):
|
def _resolve_org_filter(self, global_filters, user):
|
||||||
"""
|
"""
|
||||||
Devuelve los global_filters asegurando que siempre haya una organización.
|
Devuelve los global_filters asegurando que siempre haya una organización.
|
||||||
- Superuser sin org → error (no mezclar tenants).
|
La org se obtiene de active_organization (superuser) o del campo organizacion (usuario normal).
|
||||||
- No-superuser sin org → se inyecta la org del usuario.
|
|
||||||
Retorna (filters_dict, error_response_or_None).
|
Retorna (filters_dict, error_response_or_None).
|
||||||
"""
|
"""
|
||||||
org_value = (global_filters or {}).get('organizacion', '')
|
filters = dict(global_filters or {})
|
||||||
if not org_value:
|
if not filters.get('organizacion'):
|
||||||
if user.is_superuser:
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
return None, Response(
|
return None, Response(
|
||||||
{'error': 'El parámetro organizacion es obligatorio'},
|
{'error': 'Sin organización activa'},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
# No-superuser: inyectar su propia org
|
filters['organizacion'] = str(org.id)
|
||||||
if hasattr(user, 'organizacion') and user.organizacion:
|
return filters, None
|
||||||
filters = dict(global_filters or {})
|
|
||||||
filters['organizacion'] = str(user.organizacion.id)
|
|
||||||
return filters, None
|
|
||||||
return dict(global_filters or {}), None
|
|
||||||
|
|
||||||
def handle_simple_export(self, request):
|
def handle_simple_export(self, request):
|
||||||
"""Maneja exportación simple de DataStage (un solo modelo)"""
|
"""Maneja exportación simple de DataStage (un solo modelo)"""
|
||||||
@@ -1868,7 +1836,11 @@ class ExportDataStageView(APIView):
|
|||||||
|
|
||||||
class ExportModelView(APIView):
|
class ExportModelView(APIView):
|
||||||
my_tags = ['Reportes']
|
my_tags = ['Reportes']
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return [IsAuthenticated(), require_permission('reportes.view')()]
|
||||||
|
return [IsAuthenticated(), require_permission('reportes.export')()]
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
@@ -1906,6 +1878,8 @@ class ExportModelView(APIView):
|
|||||||
model_name = request.data.get('model')
|
model_name = request.data.get('model')
|
||||||
fields = request.data.get('fields')
|
fields = request.data.get('fields')
|
||||||
filters = request.data.get('filters', {})
|
filters = request.data.get('filters', {})
|
||||||
|
org = get_org_context(request.user)
|
||||||
|
filters['organizacion__id'] = org.id if org else None
|
||||||
export_type = request.data.get('type', 'csv')
|
export_type = request.data.get('type', 'csv')
|
||||||
module = request.data.get('module', 'datastage')
|
module = request.data.get('module', 'datastage')
|
||||||
|
|
||||||
@@ -1917,40 +1891,12 @@ class ExportModelView(APIView):
|
|||||||
else:
|
else:
|
||||||
return export_model_to_csv(request, model_name, fields, module, filters)
|
return export_model_to_csv(request, model_name, fields, module, filters)
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
|
||||||
|
|
||||||
class ExportModelView(APIView):
|
|
||||||
my_tags = ['Reportes']
|
|
||||||
permission_classes = [IsAuthenticated & (
|
|
||||||
IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
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
|
# Resumen general para dashboard
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([
|
@permission_classes([IsAuthenticated, require_permission('reportes.view')])
|
||||||
IsAuthenticated
|
|
||||||
])
|
|
||||||
def dashboard_summary(request):
|
def dashboard_summary(request):
|
||||||
org_id = request.query_params.get('organizacion_id')
|
|
||||||
filters = {}
|
filters = {}
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
@@ -1964,18 +1910,16 @@ def dashboard_summary(request):
|
|||||||
fecha_pago_lte = request.query_params.get('fecha_pago__lte')
|
fecha_pago_lte = request.query_params.get('fecha_pago__lte')
|
||||||
contribuyente__rfc = request.query_params.get('contribuyente__rfc')
|
contribuyente__rfc = request.query_params.get('contribuyente__rfc')
|
||||||
|
|
||||||
# Si no se especifica organización y el usuario tiene organización, usarla
|
org = get_org_context(user)
|
||||||
if not org_id and hasattr(user, 'organizacion') and user.organizacion:
|
if not org:
|
||||||
org_id = user.organizacion.id
|
return Response({'error': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
# Si no es superusuario, filtrar por organización
|
filters['organizacion_id'] = org.id
|
||||||
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
|
# Importador: filtrar solo por sus RFC asignados
|
||||||
if user.groups.filter(name='Importador').exists():
|
if user.is_importador:
|
||||||
rfc = getattr(user, 'rfc', None)
|
rfcs = list(user.rfc.values_list('rfc', flat=True))
|
||||||
if rfc:
|
if rfcs:
|
||||||
filters['contribuyente__rfc'] = rfc
|
filters['contribuyente__rfc__in'] = rfcs
|
||||||
|
|
||||||
if pedimento_app:
|
if pedimento_app:
|
||||||
filters['pedimento_app'] = pedimento_app
|
filters['pedimento_app'] = pedimento_app
|
||||||
|
|||||||
@@ -1,53 +1,54 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
from rest_framework import viewsets, filters
|
from rest_framework import viewsets, filters
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from api.logger.mixins import LoggingMixin
|
from api.logger.mixins import LoggingMixin
|
||||||
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin
|
from core.permissions import require_permission, user_has_permission, IsInternalService
|
||||||
|
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
|
||||||
from .models import Task
|
from .models import Task
|
||||||
from .serializers import TaskSerializer
|
from .serializers import TaskSerializer
|
||||||
from .filters import TaskFilter
|
from .filters import TaskFilter
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
from core.permissions import (
|
|
||||||
IsSameOrganization,
|
|
||||||
IsSameOrganizationDeveloper,
|
|
||||||
IsSameOrganizationAndAdmin,
|
|
||||||
IsSuperUser
|
|
||||||
)
|
|
||||||
|
|
||||||
class TaskPagination(PageNumberPagination):
|
class TaskPagination(PageNumberPagination):
|
||||||
page_size = 10
|
page_size = 10
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
max_page_size = 100
|
max_page_size = 100
|
||||||
|
|
||||||
class TaskViewSet(LoggingMixin,viewsets.ModelViewSet,OrganizacionFiltradaMixin):
|
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
class TaskViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||||
|
# Task se relaciona con pedimento, que tiene contribuyente
|
||||||
|
campo_contribuyente = 'pedimento__contribuyente'
|
||||||
|
|
||||||
queryset = Task.objects.select_related('pedimento', 'servicio').all()
|
queryset = Task.objects.select_related('pedimento', 'servicio').all()
|
||||||
serializer_class = TaskSerializer
|
serializer_class = TaskSerializer
|
||||||
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
|
||||||
filterset_class = TaskFilter
|
filterset_class = TaskFilter
|
||||||
pagination_class = TaskPagination
|
pagination_class = TaskPagination
|
||||||
ordering_fields = ['timestamp']
|
ordering_fields = ['timestamp']
|
||||||
ordering = ['-timestamp'] # ordenamiento por defecto, más reciente primero
|
ordering = ['-timestamp']
|
||||||
|
|
||||||
my_tags = ['tasks']
|
my_tags = ['tasks']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_permissions(self):
|
||||||
|
# Escritura: exclusivo para microservicio interno (Token + superuser)
|
||||||
|
# Lectura: usuarios con pedimentos.view via JWT
|
||||||
|
if self.action in ('create', 'update', 'partial_update', 'destroy'):
|
||||||
|
return [IsAuthenticated(), IsInternalService()]
|
||||||
|
return [IsAuthenticated(), require_permission('pedimentos.view')()]
|
||||||
|
|
||||||
"""
|
def get_queryset(self):
|
||||||
Filtra las tareas según la organización del usuario.
|
user = self.request.user
|
||||||
Superusuarios pueden ver todas las tareas.
|
# Service account (Token + superuser): sin filtro de org, accede a todas las tasks
|
||||||
"""
|
if user.is_superuser and isinstance(
|
||||||
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
|
getattr(self.request, 'successful_authenticator', None), TokenAuthentication
|
||||||
# if user.is_superuser:
|
):
|
||||||
# return self.queryset
|
return Task.objects.select_related('pedimento', 'servicio').all()
|
||||||
# # return self.queryset.filter(organizacion_id=user.organizacion.id)
|
if not user_has_permission(user, 'pedimentos.view'):
|
||||||
# else:
|
return Task.objects.none()
|
||||||
# return self.queryset.filter(organizacion_id=user.organizacion.id)
|
return self.get_queryset_filtrado_por_organizacion()
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -57,20 +58,82 @@ from celery.result import AsyncResult
|
|||||||
|
|
||||||
|
|
||||||
class TaskStatusView(APIView):
|
class TaskStatusView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, require_permission('pedimentos.view')]
|
||||||
|
|
||||||
|
# Mapeo de status del microservicio → estados estándar
|
||||||
|
_STATUS_MAP = {
|
||||||
|
'failed': 'FAILURE',
|
||||||
|
'completed': 'SUCCESS',
|
||||||
|
'processing': 'STARTED',
|
||||||
|
'submitted': 'PENDING',
|
||||||
|
'pending': 'PENDING',
|
||||||
|
}
|
||||||
|
|
||||||
def get(self, request, task_id):
|
def get(self, request, task_id):
|
||||||
"""
|
"""
|
||||||
Consulta el estado de una tarea Celery.
|
Consulta el estado de una tarea.
|
||||||
|
|
||||||
|
Fuente de verdad: registro Django Task (actualizado por el microservicio vía PUT).
|
||||||
|
Celery AsyncResult se usa como complemento para tareas de auditoría masiva (SUCCESS)
|
||||||
|
y como fallback cuando la tarea no está en la BD todavía.
|
||||||
|
|
||||||
Estados posibles:
|
Estados posibles:
|
||||||
PENDING — en cola, aún no inició
|
PENDING — en cola o aún no registrada
|
||||||
STARTED — worker la tomó y está ejecutando
|
STARTED — worker ejecutando
|
||||||
SUCCESS — terminó correctamente, `result` contiene el resumen
|
SUCCESS — completada sin errores
|
||||||
FAILURE — lanzó una excepción no capturada, `error` describe el problema
|
FAILURE — terminó con error
|
||||||
RETRY — el worker la está reintentando
|
RETRY — el worker la está reintentando
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Prioridad 1: Django Task record (fuente de verdad del microservicio)
|
||||||
|
try:
|
||||||
|
django_task = Task.objects.get(task_id=task_id)
|
||||||
|
effective_state = self._STATUS_MAP.get(
|
||||||
|
django_task.status.lower(), django_task.status.upper()
|
||||||
|
)
|
||||||
|
is_terminal = effective_state in ('SUCCESS', 'FAILURE')
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'task_id': task_id,
|
||||||
|
'status': effective_state,
|
||||||
|
'ready': is_terminal,
|
||||||
|
'successful': (effective_state == 'SUCCESS') if is_terminal else None,
|
||||||
|
'message': django_task.message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if effective_state == 'FAILURE':
|
||||||
|
response_data['error'] = django_task.message
|
||||||
|
|
||||||
|
elif effective_state == 'SUCCESS':
|
||||||
|
# Para auditoría masiva, intentar enriquecer con resultado de Celery
|
||||||
|
try:
|
||||||
|
celery_result = AsyncResult(task_id)
|
||||||
|
if celery_result.ready() and celery_result.successful():
|
||||||
|
result = celery_result.result
|
||||||
|
response_data['result'] = result
|
||||||
|
if isinstance(result, dict) and 'total_pedimentos' in result:
|
||||||
|
total = result.get('total_pedimentos', 0)
|
||||||
|
completados = result.get('completados', 0)
|
||||||
|
con_pendientes = result.get('con_pendientes', 0)
|
||||||
|
con_errores = result.get('con_errores', 0)
|
||||||
|
if con_pendientes == 0 and con_errores == 0:
|
||||||
|
response_data['mensaje'] = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes'
|
||||||
|
else:
|
||||||
|
partes = []
|
||||||
|
if con_pendientes:
|
||||||
|
partes.append(f'{con_pendientes} con documentos pendientes')
|
||||||
|
if con_errores:
|
||||||
|
partes.append(f'{con_errores} con error')
|
||||||
|
response_data['mensaje'] = f'{completados}/{total} pedimentos completos — {", ".join(partes)}'
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Response(response_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Prioridad 2: Celery AsyncResult (tarea aún no registrada en BD)
|
||||||
task_result = AsyncResult(task_id)
|
task_result = AsyncResult(task_id)
|
||||||
state = task_result.state
|
state = task_result.state
|
||||||
|
|
||||||
@@ -84,25 +147,20 @@ class TaskStatusView(APIView):
|
|||||||
if state == 'SUCCESS':
|
if state == 'SUCCESS':
|
||||||
result = task_result.result
|
result = task_result.result
|
||||||
response_data['result'] = result
|
response_data['result'] = result
|
||||||
|
|
||||||
# Resumen legible cuando es auditoría masiva de organización
|
|
||||||
if isinstance(result, dict) and 'total_pedimentos' in result:
|
if isinstance(result, dict) and 'total_pedimentos' in result:
|
||||||
total = result.get('total_pedimentos', 0)
|
total = result.get('total_pedimentos', 0)
|
||||||
completados = result.get('completados', 0)
|
completados = result.get('completados', 0)
|
||||||
con_pendientes = result.get('con_pendientes', 0)
|
con_pendientes = result.get('con_pendientes', 0)
|
||||||
con_errores = result.get('con_errores', 0)
|
con_errores = result.get('con_errores', 0)
|
||||||
|
|
||||||
if con_pendientes == 0 and con_errores == 0:
|
if con_pendientes == 0 and con_errores == 0:
|
||||||
mensaje = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes'
|
response_data['mensaje'] = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes'
|
||||||
else:
|
else:
|
||||||
partes = []
|
partes = []
|
||||||
if con_pendientes:
|
if con_pendientes:
|
||||||
partes.append(f'{con_pendientes} con documentos pendientes')
|
partes.append(f'{con_pendientes} con documentos pendientes')
|
||||||
if con_errores:
|
if con_errores:
|
||||||
partes.append(f'{con_errores} con error')
|
partes.append(f'{con_errores} con error')
|
||||||
mensaje = f'{completados}/{total} pedimentos completos — {", ".join(partes)}'
|
response_data['mensaje'] = f'{completados}/{total} pedimentos completos — {", ".join(partes)}'
|
||||||
|
|
||||||
response_data['mensaje'] = mensaje
|
|
||||||
|
|
||||||
elif state == 'FAILURE':
|
elif state == 'FAILURE':
|
||||||
response_data['error'] = str(task_result.info)
|
response_data['error'] = str(task_result.info)
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ class VucemUpdateSerializer(VucemSerializer):
|
|||||||
class Meta(VucemSerializer.Meta):
|
class Meta(VucemSerializer.Meta):
|
||||||
fields = VucemSerializer.Meta.fields
|
fields = VucemSerializer.Meta.fields
|
||||||
from .models import Vucem, CredencialesImportador
|
from .models import Vucem, CredencialesImportador
|
||||||
from core.permissions import IsSameOrganizationDeveloper
|
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
|
|
||||||
from core.permissions import (
|
from core.permissions import (
|
||||||
IsSameOrganization,
|
IsSameOrganizationAndInAllowedGroups,
|
||||||
IsSameOrganizationDeveloper,
|
get_org_context,
|
||||||
IsSameOrganizationAndAdmin,
|
is_internal_service_request,
|
||||||
IsSuperUser,
|
require_permission,
|
||||||
IsSameOrganizationAndInAllowedGroups
|
user_has_permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
class CustomVucemPagination(PageNumberPagination):
|
class CustomVucemPagination(PageNumberPagination):
|
||||||
@@ -53,8 +52,6 @@ class CustomVucemPagination(PageNumberPagination):
|
|||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
class VucemView(viewsets.ModelViewSet):
|
class VucemView(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated , (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
|
|
||||||
|
|
||||||
queryset = Vucem.objects.all()
|
queryset = Vucem.objects.all()
|
||||||
pagination_class = CustomVucemPagination
|
pagination_class = CustomVucemPagination
|
||||||
filterset_fields = ['organizacion', 'patente', 'usuario', 'is_importador', 'acusecove', 'acuseedocument', 'is_active']
|
filterset_fields = ['organizacion', 'patente', 'usuario', 'is_importador', 'acusecove', 'acuseedocument', 'is_active']
|
||||||
@@ -68,27 +65,45 @@ class VucemView(viewsets.ModelViewSet):
|
|||||||
return VucemSerializer
|
return VucemSerializer
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.action in ['create', 'update', 'partial_update', 'destroy']:
|
perms = {
|
||||||
return [IsAuthenticated(), IsSameOrganizationAndInAllowedGroups()]
|
'list': 'vucem.view',
|
||||||
return super().get_permissions()
|
'retrieve': 'vucem.view',
|
||||||
|
'create': 'vucem.manage',
|
||||||
|
'update': 'vucem.manage',
|
||||||
|
'partial_update': 'vucem.manage',
|
||||||
|
'destroy': 'vucem.manage',
|
||||||
|
'download_cer': 'vucem.view',
|
||||||
|
'download_key': 'vucem.view',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'vucem.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Verificar que el usuario esté autenticado y tenga organización
|
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return self.queryset.none()
|
return self.queryset.none()
|
||||||
|
|
||||||
queryset = self.queryset
|
if is_internal_service_request(self.request):
|
||||||
|
queryset = self.queryset.all()
|
||||||
|
importador_rfc = self.request.query_params.get('importador')
|
||||||
|
if importador_rfc:
|
||||||
|
queryset = queryset.filter(usuarios_importadores__rfc__rfc=importador_rfc).distinct()
|
||||||
|
return queryset
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
if not user_has_permission(self.request.user, 'vucem.view'):
|
||||||
queryset = queryset.all()
|
return self.queryset.none()
|
||||||
elif not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
|
|
||||||
return queryset.none()
|
org = get_org_context(self.request.user)
|
||||||
elif self.request.user.groups.filter(name='Importador').exists():
|
if not org:
|
||||||
queryset = queryset.filter(organizacion=self.request.user.organizacion, usuario__in=self.request.user.rfc.all())
|
return self.queryset.none()
|
||||||
|
|
||||||
|
if self.request.user.is_importador:
|
||||||
|
queryset = self.queryset.filter(
|
||||||
|
organizacion=org,
|
||||||
|
usuario__in=self.request.user.rfc.all(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(organizacion=self.request.user.organizacion)
|
queryset = self.queryset.filter(organizacion=org)
|
||||||
|
|
||||||
# Filtro por importador (RFC)
|
|
||||||
importador_rfc = self.request.query_params.get('importador')
|
importador_rfc = self.request.query_params.get('importador')
|
||||||
if importador_rfc:
|
if importador_rfc:
|
||||||
queryset = queryset.filter(usuarios_importadores__rfc__rfc=importador_rfc).distinct()
|
queryset = queryset.filter(usuarios_importadores__rfc__rfc=importador_rfc).distinct()
|
||||||
@@ -96,54 +111,37 @@ class VucemView(viewsets.ModelViewSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if is_internal_service_request(self.request):
|
||||||
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
|
serializer.save(updated_by=self.request.user)
|
||||||
if self.request.user.is_superuser:
|
|
||||||
organizacion_id = self.request.data.get('organizacion_id')
|
|
||||||
|
|
||||||
if not organizacion_id:
|
|
||||||
raise ValueError("Los superusuarios deben especificar una organización")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Importa el modelo Organizacion
|
|
||||||
# from ..organization.models import Organizacion
|
|
||||||
organizacion = Organizacion.objects.get(id=organizacion_id)
|
|
||||||
except Organizacion.DoesNotExist:
|
|
||||||
raise ValueError({"organizacion": "Organización no encontrada"})
|
|
||||||
|
|
||||||
serializer.save(
|
|
||||||
organizacion=organizacion,
|
|
||||||
created_by=self.request.user,
|
|
||||||
updated_by=self.request.user
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
else:
|
org = get_org_context(self.request.user)
|
||||||
serializer.save(
|
if not org:
|
||||||
organizacion=self.request.user.organizacion,
|
raise ValueError("El usuario debe tener una organización activa para crear credenciales VUCEM.")
|
||||||
created_by=self.request.user,
|
serializer.save(
|
||||||
updated_by=self.request.user
|
organizacion=org,
|
||||||
)
|
created_by=self.request.user,
|
||||||
return
|
updated_by=self.request.user,
|
||||||
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if is_internal_service_request(self.request):
|
||||||
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
|
instance = self.get_object()
|
||||||
|
serializer.save(
|
||||||
|
created_by=instance.created_by,
|
||||||
|
updated_by=self.request.user,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
raise ValueError("El usuario debe tener una organización activa para modificar credenciales VUCEM.")
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
if self.request.user.is_superuser:
|
serializer.save(
|
||||||
serializer.save(
|
organizacion=org,
|
||||||
created_by=instance.created_by,
|
created_by=instance.created_by,
|
||||||
updated_by=self.request.user
|
updated_by=self.request.user,
|
||||||
)
|
)
|
||||||
return
|
|
||||||
else:
|
|
||||||
serializer.save(
|
|
||||||
organizacion=self.request.user.organizacion,
|
|
||||||
created_by=instance.created_by,
|
|
||||||
updated_by=self.request.user
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
|
@action(detail=True, methods=["get"])
|
||||||
def download_cer(self, request, pk=None):
|
def download_cer(self, request, pk=None):
|
||||||
vucem = self.get_object()
|
vucem = self.get_object()
|
||||||
if not vucem.cer:
|
if not vucem.cer:
|
||||||
@@ -164,7 +162,7 @@ class VucemView(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
|
@action(detail=True, methods=["get"])
|
||||||
def download_key(self, request, pk=None):
|
def download_key(self, request, pk=None):
|
||||||
vucem = self.get_object()
|
vucem = self.get_object()
|
||||||
if not vucem.key:
|
if not vucem.key:
|
||||||
@@ -194,7 +192,6 @@ class VucemView(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class CredencialesImportadorViewSet(viewsets.ModelViewSet):
|
class CredencialesImportadorViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
queryset = CredencialesImportador.objects.all()
|
queryset = CredencialesImportador.objects.all()
|
||||||
serializer_class = CredencialesImportadorSimpleSerializer
|
serializer_class = CredencialesImportadorSimpleSerializer
|
||||||
filterset_fields = ['organizacion', 'vucem', 'rfc']
|
filterset_fields = ['organizacion', 'vucem', 'rfc']
|
||||||
@@ -205,27 +202,34 @@ class CredencialesImportadorViewSet(viewsets.ModelViewSet):
|
|||||||
my_tags = ['Credenciales por Importador']
|
my_tags = ['Credenciales por Importador']
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.action in ['create', 'update', 'partial_update', 'destroy']:
|
perms = {
|
||||||
return [IsAuthenticated()]
|
'list': 'vucem.view',
|
||||||
return super().get_permissions()
|
'retrieve': 'vucem.view',
|
||||||
|
'create': 'vucem.manage',
|
||||||
|
'update': 'vucem.manage',
|
||||||
|
'partial_update': 'vucem.manage',
|
||||||
|
'destroy': 'vucem.manage',
|
||||||
|
}
|
||||||
|
codename = perms.get(self.action, 'vucem.view')
|
||||||
|
return [IsAuthenticated(), require_permission(codename)()]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
if self.request.user.is_superuser:
|
|
||||||
# Si es superusuario, devolver todos los registros
|
|
||||||
return self.queryset.all()
|
|
||||||
|
|
||||||
# Verificar que el usuario esté autenticado y tenga organización
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
|
||||||
return self.queryset.none()
|
return self.queryset.none()
|
||||||
|
if is_internal_service_request(self.request):
|
||||||
queryset = self.queryset.filter(organizacion=self.request.user.organizacion)
|
return self.queryset.all()
|
||||||
|
if not user_has_permission(self.request.user, 'vucem.view'):
|
||||||
|
return self.queryset.none()
|
||||||
return queryset
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
return self.queryset.none()
|
||||||
|
return self.queryset.filter(organizacion=org)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if is_internal_service_request(self.request):
|
||||||
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
|
serializer.save()
|
||||||
serializer.save(organizacion=self.request.user.organizacion)
|
return
|
||||||
return
|
org = get_org_context(self.request.user)
|
||||||
|
if not org:
|
||||||
|
raise ValueError("El usuario debe tener una organización activa.")
|
||||||
|
serializer.save(organizacion=org)
|
||||||
@@ -98,6 +98,7 @@ OWN_APPS = [
|
|||||||
'api.organization',
|
'api.organization',
|
||||||
'api.licence',
|
'api.licence',
|
||||||
'api.cuser',
|
'api.cuser',
|
||||||
|
'api.rbac',
|
||||||
'api.datastage',
|
'api.datastage',
|
||||||
'api.vucem',
|
'api.vucem',
|
||||||
'api.logger',
|
'api.logger',
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ urlpatterns = [
|
|||||||
path('api/v1/cards/', include('api.cards.urls')), # Cards app
|
path('api/v1/cards/', include('api.cards.urls')), # Cards app
|
||||||
path('api/v1/reports/', include('api.reports.urls')), # Reports app
|
path('api/v1/reports/', include('api.reports.urls')), # Reports app
|
||||||
path('api/v1/tasks/', include('api.tasks.urls')), # Tasks app
|
path('api/v1/tasks/', include('api.tasks.urls')), # Tasks app
|
||||||
|
path('api/v1/rbac/', include('api.rbac.urls')), # RBAC app
|
||||||
]
|
]
|
||||||
# En producción, los archivos media son servidos por Nginx
|
# En producción, los archivos media son servidos por Nginx
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
@@ -1,100 +1,244 @@
|
|||||||
# permissions.py
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from api.cuser.models import CustomUser
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
class IsSameOrganization(permissions.BasePermission):
|
|
||||||
"""
|
|
||||||
Permiso personalizado que solo permite acceder a usuarios de la misma organización
|
|
||||||
o a administradores/staff.
|
|
||||||
"""
|
|
||||||
def has_permission(self, request, view):
|
|
||||||
# Permite listar/crear solo si el usuario está autenticado
|
|
||||||
return request.user.is_authenticated
|
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
# ---------------------------------------------------------------------------
|
||||||
# Permite operaciones sobre un objeto específico solo si:
|
# Helpers centrales — toda la lógica de RBAC pasa por aquí
|
||||||
# - El objeto pertenece a la misma organización (acceso por usuario relacionado)
|
# ---------------------------------------------------------------------------
|
||||||
return (getattr(obj, 'dirigido', None) and obj.dirigido.organizacion == request.user.organizacion)
|
|
||||||
|
|
||||||
class IsSameOrganizationAndAdmin(permissions.BasePermission):
|
def is_internal_service_request(request):
|
||||||
"""
|
"""True si la petición proviene de un service account (Token auth + superuser).
|
||||||
Permiso personalizado que solo permite acceder a usuarios de la misma organización
|
Misma lógica que IsInternalService, útil en get_queryset() y perform_* methods."""
|
||||||
o a administradores/staff.
|
user = getattr(request, 'user', None)
|
||||||
"""
|
if not user or not user.is_superuser:
|
||||||
def has_permission(self, request, view):
|
return False
|
||||||
# Permite listar/crear solo si el usuario está autenticado
|
return isinstance(getattr(request, 'successful_authenticator', None), TokenAuthentication)
|
||||||
return request.user.is_authenticated
|
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
|
||||||
# Permite operaciones solo si el usuario es admin, Agente Aduanal o user y la organización coincide
|
def get_org_context(user):
|
||||||
allowed_groups = ['admin', 'Agente Aduanal', 'user']
|
"""Retorna la organización activa para filtrado de datos.
|
||||||
user_in_group = request.user.groups.filter(name__in=allowed_groups).exists()
|
Superusuarios usan active_organization; usuarios normales usan organizacion."""
|
||||||
if not user_in_group:
|
if user.is_superuser:
|
||||||
return False
|
return getattr(user, 'active_organization', None)
|
||||||
if hasattr(obj, 'organizacion'):
|
return getattr(user, 'organizacion', None)
|
||||||
return obj.organizacion == request.user.organizacion
|
|
||||||
|
|
||||||
|
def user_has_permission(user, codename):
|
||||||
|
"""Verifica si un usuario tiene un permiso RBAC por su codename.
|
||||||
|
|
||||||
|
Orden de evaluación:
|
||||||
|
1. is_superuser → True siempre
|
||||||
|
2. UserPermission deny explícito → False
|
||||||
|
3. UserPermission grant explícito → True
|
||||||
|
4. Algún UserRole en su org tiene el permiso → True
|
||||||
|
5. Denegar
|
||||||
|
"""
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
org = getattr(user, 'organizacion', None)
|
||||||
|
if not org:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class IsSameOrganizationDeveloper(permissions.BasePermission):
|
from api.rbac.models import UserPermission, UserRole
|
||||||
"""
|
|
||||||
Permiso personalizado que solo permite acceder a usuarios de la misma organización
|
|
||||||
o a administradores/staff.
|
|
||||||
"""
|
|
||||||
def has_permission(self, request, view):
|
|
||||||
# Permite listar/crear solo si el usuario está autenticado
|
|
||||||
return request.user.is_authenticated
|
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
try:
|
||||||
# Permite operaciones solo si el usuario es developer, Agente Aduanal o user y la organización coincide
|
override = UserPermission.objects.get(user=user, permission__codename=codename)
|
||||||
allowed_groups = ['developer', 'Agente Aduanal', 'user']
|
return override.granted
|
||||||
user_in_group = request.user.groups.filter(name__in=allowed_groups).exists()
|
except UserPermission.DoesNotExist:
|
||||||
if not user_in_group:
|
pass
|
||||||
return False
|
|
||||||
if hasattr(obj, 'organizacion'):
|
return UserRole.objects.filter(
|
||||||
return obj.organizacion == request.user.organizacion
|
user=user,
|
||||||
|
role__organizacion=org,
|
||||||
|
role__permissions__codename=codename,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def user_has_role(user, role_name):
|
||||||
|
"""Verifica si un usuario tiene un rol por nombre dentro de su organización.
|
||||||
|
Función puente durante la transición — lee desde UserRole en lugar de auth.Group."""
|
||||||
|
from api.rbac.models import UserRole
|
||||||
|
|
||||||
|
org = getattr(user, 'organizacion', None)
|
||||||
|
if not org:
|
||||||
return False
|
return False
|
||||||
|
return UserRole.objects.filter(
|
||||||
|
user=user,
|
||||||
|
role__nombre=role_name,
|
||||||
|
role__organizacion=org,
|
||||||
|
).exists()
|
||||||
|
|
||||||
class IsOwnerOrOrgAdmin(permissions.BasePermission):
|
|
||||||
def has_object_permission(self, request, view, obj):
|
|
||||||
return (
|
|
||||||
obj == request.user or
|
|
||||||
request.user.is_staff or
|
|
||||||
request.user.groups.filter(name='admin').exists()
|
|
||||||
)
|
|
||||||
|
|
||||||
class IsSuperUser(permissions.BasePermission):
|
# ---------------------------------------------------------------------------
|
||||||
def has_object_permission(self, request, view, obj):
|
# Base compartida — aplica el requisito de org activa a superusuarios
|
||||||
return request.user.is_superuser
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class OrgScopedPermission(permissions.BasePermission):
|
||||||
|
"""Base para todas las clases de permiso con scope de organización.
|
||||||
|
Superusuario sin active_organization recibe 403, EXCEPTO service accounts
|
||||||
|
(Token auth + superuser) que pasan sin restricción de org."""
|
||||||
|
|
||||||
|
message = 'No tienes permiso para realizar esta acción.'
|
||||||
|
|
||||||
class HasStoragePermission(permissions.BasePermission):
|
|
||||||
"""
|
|
||||||
Permiso personalizado que permite el acceso a los usuarios que tienen permisos de almacenamiento.
|
|
||||||
"""
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
# Permite el acceso si el usuario tiene el permiso 'can_access_storage'
|
if not request.user.is_authenticated:
|
||||||
return request.user.has_perm('api.cuser.can_access_storage')
|
return False
|
||||||
|
if request.user.is_superuser:
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
# Service account interno: Token auth + superuser → siempre permitido
|
||||||
|
if isinstance(getattr(request, 'successful_authenticator', None), TokenAuthentication):
|
||||||
|
return True
|
||||||
|
# Superuser JWT: requiere active_organization
|
||||||
|
if not getattr(request.user, 'active_organization', None):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Clases de permiso
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class IsSameOrganization(OrgScopedPermission):
|
||||||
|
"""Usuario autenticado con org activa. Cualquier rol pasa (incluyendo Importador)."""
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
# Permite operaciones sobre un objeto específico si el usuario tiene el permiso
|
org = get_org_context(request.user)
|
||||||
return request.user.has_perm('api.cuser.can_access_storage')
|
if not org:
|
||||||
|
return False
|
||||||
|
dirigido = getattr(obj, 'dirigido', None)
|
||||||
|
if dirigido:
|
||||||
|
return getattr(dirigido, 'organizacion', None) == org
|
||||||
|
return getattr(obj, 'organizacion', None) == org
|
||||||
|
|
||||||
class IsSameOrganizationAndInAllowedGroups(permissions.BasePermission):
|
|
||||||
"""
|
class IsSameOrganizationAndAdmin(OrgScopedPermission):
|
||||||
Permite update/delete solo si el usuario está en TODOS los grupos permitidos
|
"""Usuario con rol admin, Agente Aduanal o user en su organización."""
|
||||||
y pertenece a la misma organización que el registro, o es superuser.
|
|
||||||
"""
|
|
||||||
allowed_groups = ['admin', 'Agente Aduanal', 'user']
|
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_authenticated:
|
|
||||||
return False
|
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
if not hasattr(user, 'organizacion') or not user.organizacion:
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
return False
|
return False
|
||||||
# Debe tener los tres grupos asignados
|
tiene_rol = (
|
||||||
for group in self.allowed_groups:
|
user_has_role(user, 'admin') or
|
||||||
if not user.groups.filter(name=group).exists():
|
user_has_role(user, 'Agente Aduanal') or
|
||||||
|
user_has_role(user, 'user')
|
||||||
|
)
|
||||||
|
if not tiene_rol:
|
||||||
|
return False
|
||||||
|
return getattr(obj, 'organizacion', None) == org
|
||||||
|
|
||||||
|
|
||||||
|
class IsSameOrganizationDeveloper(OrgScopedPermission):
|
||||||
|
"""Usuario con rol developer, Agente Aduanal o user en su organización."""
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
user = request.user
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return False
|
||||||
|
tiene_rol = (
|
||||||
|
user_has_role(user, 'developer') or
|
||||||
|
user_has_role(user, 'Agente Aduanal') or
|
||||||
|
user_has_role(user, 'user')
|
||||||
|
)
|
||||||
|
if not tiene_rol:
|
||||||
|
return False
|
||||||
|
return getattr(obj, 'organizacion', None) == org
|
||||||
|
|
||||||
|
|
||||||
|
class IsOwnerOrOrgAdmin(OrgScopedPermission):
|
||||||
|
"""El propio usuario, staff de Django o usuario con rol admin en la org."""
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
user = request.user
|
||||||
|
return (
|
||||||
|
obj == user or
|
||||||
|
user.is_staff or
|
||||||
|
user.is_superuser or
|
||||||
|
user_has_role(user, 'admin')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IsSuperUser(permissions.BasePermission):
|
||||||
|
"""Solo superusuarios de Django. No requiere org activa (para endpoints de gestión global)."""
|
||||||
|
|
||||||
|
message = 'No tienes permiso para realizar esta acción.'
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return request.user.is_authenticated and request.user.is_superuser
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
class IsInternalService(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
Identifica llamadas internas de microservicio → backend.
|
||||||
|
|
||||||
|
Criterio: autenticación via Token (no JWT) + usuario superuser.
|
||||||
|
Esto garantiza que solo cuentas de servicio predefinidas pasan,
|
||||||
|
sin depender de flags manuales como is_staff que pueden no estar
|
||||||
|
configurados en producción.
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = 'Acceso reservado para servicios internos.'
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not request.user or not request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
return (
|
||||||
|
isinstance(request.successful_authenticator, TokenAuthentication)
|
||||||
|
and request.user.is_superuser
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HasStoragePermission(OrgScopedPermission):
|
||||||
|
"""Usuarios con acceso a operaciones de almacenamiento (organizacion.view)."""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not super().has_permission(request, view):
|
||||||
|
return False
|
||||||
|
return user_has_permission(request.user, 'organizacion.view')
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return user_has_permission(request.user, 'organizacion.view')
|
||||||
|
|
||||||
|
|
||||||
|
def require_permission(codename):
|
||||||
|
"""
|
||||||
|
Devuelve una clase de permiso DRF que exige el codename RBAC indicado.
|
||||||
|
Uso en permission_classes: require_permission('pedimentos.view')
|
||||||
|
Uso en get_permissions(): require_permission('pedimentos.create')()
|
||||||
|
"""
|
||||||
|
class _RbacPerm(OrgScopedPermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not super().has_permission(request, view):
|
||||||
return False
|
return False
|
||||||
return obj.organizacion == user.organizacion
|
return user_has_permission(request.user, codename)
|
||||||
|
_RbacPerm.__name__ = f'HasPerm_{codename.replace(".", "_")}'
|
||||||
|
_RbacPerm.__qualname__ = _RbacPerm.__name__
|
||||||
|
return _RbacPerm
|
||||||
|
|
||||||
|
|
||||||
|
class IsSameOrganizationAndInAllowedGroups(OrgScopedPermission):
|
||||||
|
"""Usuario con permiso vucem.manage en su organización.
|
||||||
|
Reemplaza la lógica rota que requería 3 grupos simultáneamente."""
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
user = request.user
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return False
|
||||||
|
if not user_has_permission(user, 'vucem.manage'):
|
||||||
|
return False
|
||||||
|
return getattr(obj, 'organizacion', None) == org
|
||||||
|
|||||||
@@ -1,142 +1,179 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from core.permissions import get_org_context, user_has_role, is_internal_service_request
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_internal_service(request):
|
||||||
|
return is_internal_service_request(request)
|
||||||
|
|
||||||
|
|
||||||
class FiltroPorOrganizacionMixin:
|
class FiltroPorOrganizacionMixin:
|
||||||
model = None
|
model = None
|
||||||
campo_usuario = 'user'
|
campo_usuario = 'user'
|
||||||
campo_organizacion = 'organizacion'
|
campo_organizacion = 'organizacion'
|
||||||
campo_rfc = 'rfc__id'
|
campo_contribuyente = 'pedimento__contribuyente'
|
||||||
campo_contribuyente = 'pedimento__contribuyente' # solo si aplica
|
|
||||||
|
|
||||||
def get_queryset_filtrado(self):
|
def get_queryset_filtrado(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
if not user.is_authenticated or not hasattr(user, self.campo_organizacion):
|
if not user.is_authenticated:
|
||||||
return self.model.objects.none()
|
return self.model.objects.none()
|
||||||
|
|
||||||
if user.is_superuser:
|
if _is_internal_service(self.request):
|
||||||
return self.model.objects.all()
|
return self.model.objects.all()
|
||||||
|
|
||||||
if (user.groups.filter(name='admin').exists() or user.groups.filter(name='developer').exists()) and user.is_authenticated and user.groups.filter(name='Agente Aduanal').exists():
|
org = get_org_context(user)
|
||||||
model_fields = [f.name for f in self.model._meta.get_fields()]
|
if not org:
|
||||||
if self.campo_organizacion in model_fields:
|
return self.model.objects.none()
|
||||||
filtro = {f"{self.campo_organizacion}": getattr(user, self.campo_organizacion)}
|
|
||||||
else:
|
filtro = {self.campo_organizacion: org}
|
||||||
return self.model.objects.none()
|
|
||||||
|
# Superuser y usuarios con rol operativo ven todo lo de su org activa
|
||||||
|
if user.is_superuser:
|
||||||
return self.model.objects.filter(**filtro)
|
return self.model.objects.filter(**filtro)
|
||||||
|
|
||||||
if user.groups.filter(name='Importador').exists() and getattr(user, 'is_importador', False):
|
if (
|
||||||
filtro = {
|
user_has_role(user, 'admin') or
|
||||||
f"{self.campo_contribuyente}__{self.campo_rfc}": getattr(user, self.campo_rfc),
|
user_has_role(user, 'developer') or
|
||||||
}
|
user_has_role(user, 'Agente Aduanal') or
|
||||||
|
user_has_role(user, 'user')
|
||||||
|
):
|
||||||
|
return self.model.objects.filter(**filtro)
|
||||||
|
|
||||||
|
# Importador: acceso filtrado por org + RFC como contribuyente
|
||||||
|
if user.is_importador:
|
||||||
|
filtro[f"{self.campo_contribuyente}__in"] = user.rfc.all()
|
||||||
return self.model.objects.filter(**filtro)
|
return self.model.objects.filter(**filtro)
|
||||||
|
|
||||||
return self.model.objects.none()
|
return self.model.objects.none()
|
||||||
|
|
||||||
# en core/mixins/organizacion.py o similar
|
|
||||||
class OrganizacionFiltradaMixin:
|
class OrganizacionFiltradaMixin:
|
||||||
model = None # Puedes sobreescribir esto en la vista
|
model = None
|
||||||
campo_organizacion = 'organizacion'
|
campo_organizacion = 'organizacion'
|
||||||
campo_contribuyente = 'contribuyente' # solo si aplica
|
campo_contribuyente = 'contribuyente'
|
||||||
|
|
||||||
def get_queryset_filtrado_por_organizacion(self):
|
def get_queryset_filtrado_por_organizacion(self):
|
||||||
model = self.model or self.queryset.model
|
model = self.model or self.queryset.model
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if not user.is_authenticated:
|
||||||
return model.objects.none()
|
return model.objects.none()
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
if _is_internal_service(self.request):
|
||||||
return model.objects.all()
|
return model.objects.all()
|
||||||
|
|
||||||
org = self.request.user.organizacion
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return model.objects.none()
|
||||||
|
|
||||||
filtros_base = {
|
filtros_base = {
|
||||||
f"{self.campo_organizacion}": org,
|
self.campo_organizacion: org,
|
||||||
f"{self.campo_organizacion}__is_active": True,
|
f'{self.campo_organizacion}__is_active': True,
|
||||||
f"{self.campo_organizacion}__is_verified": True,
|
f'{self.campo_organizacion}__is_verified': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
grupos = self.request.user.groups.values_list('name', flat=True)
|
if user.is_superuser:
|
||||||
|
return model.objects.filter(**filtros_base)
|
||||||
if self.request.user.is_authenticated and 'Agente Aduanal' in grupos and (('admin' in grupos or 'developer' in grupos) and 'user' in grupos) :
|
|
||||||
if 'Agente Aduanal' in grupos:
|
if (
|
||||||
return model.objects.filter(**filtros_base)
|
user_has_role(user, 'admin') or
|
||||||
|
user_has_role(user, 'developer') or
|
||||||
# if hasattr(model, self.campo_contribuyente):
|
user_has_role(user, 'Agente Aduanal') or
|
||||||
if self.request.user.is_authenticated and 'Importador' in grupos:
|
user_has_role(user, 'user')
|
||||||
filtros_base[f"{self.campo_contribuyente}__in"] = self.request.user.rfc.all()
|
):
|
||||||
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
|
if user.is_importador:
|
||||||
|
filtros_base[f'{self.campo_contribuyente}__in'] = user.rfc.all()
|
||||||
return model.objects.filter(**filtros_base)
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
# Si no entra en los roles válidos
|
|
||||||
return model.objects.none()
|
return model.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class DocumentosFiltradosMixin:
|
class DocumentosFiltradosMixin:
|
||||||
model = None
|
model = None
|
||||||
campo_organizacion = 'organizacion'
|
campo_organizacion = 'organizacion'
|
||||||
campo_contribuyente = 'pedimento' # solo si aplica
|
campo_contribuyente = 'pedimento'
|
||||||
|
|
||||||
def get_queryset_filtrado_por_organizacion(self):
|
def get_queryset_filtrado_por_organizacion(self):
|
||||||
model = self.model or self.queryset.model
|
model = self.model or self.queryset.model
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if not user.is_authenticated:
|
||||||
return model.objects.none()
|
return model.objects.none()
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
if _is_internal_service(self.request):
|
||||||
return model.objects.all()
|
return model.objects.all()
|
||||||
|
|
||||||
org = self.request.user.organizacion
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return model.objects.none()
|
||||||
|
|
||||||
filtros_base = {
|
filtros_base = {
|
||||||
f"{self.campo_organizacion}": org.id,
|
f'{self.campo_organizacion}': org.id,
|
||||||
f"{self.campo_organizacion}__is_active": True,
|
f'{self.campo_organizacion}__is_active': True,
|
||||||
f"{self.campo_organizacion}__is_verified": True,
|
f'{self.campo_organizacion}__is_verified': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
grupos = self.request.user.groups.values_list('name', flat=True)
|
if user.is_superuser:
|
||||||
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
if self.request.user.is_authenticated and 'Agente Aduanal' in grupos and ('admin' in grupos or 'developer' in grupos or 'user' in grupos):
|
if (
|
||||||
if 'Agente Aduanal' in grupos:
|
user_has_role(user, 'admin') or
|
||||||
return model.objects.filter(**filtros_base)
|
user_has_role(user, 'developer') or
|
||||||
|
user_has_role(user, 'Agente Aduanal') or
|
||||||
|
user_has_role(user, 'user')
|
||||||
|
):
|
||||||
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
if hasattr(model, self.campo_contribuyente):
|
if user.is_importador:
|
||||||
if self.request.user.is_authenticated and 'Importador' in grupos and getattr(self.request.user, 'is_importador', False):
|
filtros_base[f'{self.campo_contribuyente}__contribuyente__in'] = user.rfc.all()
|
||||||
filtros_base[f"{self.campo_contribuyente}__contribuyente__in"] = self.request.user.rfc.all()
|
return model.objects.filter(**filtros_base)
|
||||||
return model.objects.filter(**filtros_base)
|
|
||||||
|
|
||||||
# Si no entra en los roles válidos
|
|
||||||
return model.objects.none()
|
return model.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class ProcesosPorOrganizacionMixin:
|
class ProcesosPorOrganizacionMixin:
|
||||||
model = None # Puedes sobreescribir esto en la vista
|
model = None
|
||||||
campo_organizacion = 'organizacion'
|
campo_organizacion = 'organizacion'
|
||||||
campo_pedimento = 'pedimento' # solo si aplica
|
campo_pedimento = 'pedimento'
|
||||||
|
|
||||||
def get_queryset_filtrado_por_organizacion(self):
|
def get_queryset_filtrado_por_organizacion(self):
|
||||||
model = self.model or self.queryset.model
|
model = self.model or self.queryset.model
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
if not user.is_authenticated:
|
||||||
return model.objects.none()
|
return model.objects.none()
|
||||||
|
|
||||||
if self.request.user.is_superuser:
|
if _is_internal_service(self.request):
|
||||||
return model.objects.all()
|
return model.objects.all()
|
||||||
|
|
||||||
org = self.request.user.organizacion
|
org = get_org_context(user)
|
||||||
|
if not org:
|
||||||
|
return model.objects.none()
|
||||||
|
|
||||||
filtros_base = {
|
filtros_base = {
|
||||||
f"{self.campo_organizacion}": org,
|
self.campo_organizacion: org,
|
||||||
f"{self.campo_organizacion}__is_active": True,
|
f'{self.campo_organizacion}__is_active': True,
|
||||||
f"{self.campo_organizacion}__is_verified": True,
|
f'{self.campo_organizacion}__is_verified': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
grupos = self.request.user.groups.values_list('name', flat=True)
|
if user.is_superuser:
|
||||||
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
|
if (
|
||||||
|
user_has_role(user, 'admin') or
|
||||||
|
user_has_role(user, 'developer') or
|
||||||
|
user_has_role(user, 'Agente Aduanal') or
|
||||||
|
user_has_role(user, 'user')
|
||||||
|
):
|
||||||
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
if self.request.user.is_authenticated and 'Agente Aduanal' in grupos and ('admin' in grupos or 'developer' in grupos or 'user' in grupos) :
|
if user.is_importador:
|
||||||
if 'Agente Aduanal' in grupos:
|
filtros_base[f'{self.campo_pedimento}__contribuyente__in'] = user.rfc.all()
|
||||||
return model.objects.filter(**filtros_base)
|
return model.objects.filter(**filtros_base)
|
||||||
|
|
||||||
if hasattr(model, self.campo_pedimento):
|
|
||||||
if self.request.user.is_authenticated and 'Importador' in grupos and getattr(self.request.user, 'is_importador', False):
|
|
||||||
filtros_base[f"{self.campo_pedimento}__contribuyente__in"] = self.request.user.rfc.all()
|
|
||||||
return model.objects.filter(**filtros_base)
|
|
||||||
|
|
||||||
# Si no entra en los roles válidos
|
|
||||||
return model.objects.none()
|
return model.objects.none()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user