Mudanza de repo
This commit is contained in:
0
api/organization/__init__.py
Normal file
0
api/organization/__init__.py
Normal file
18
api/organization/admin.py
Normal file
18
api/organization/admin.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.contrib import admin
|
||||
from .models import Organizacion
|
||||
# Register your models here.
|
||||
|
||||
class OrganizacionAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'nombre', 'rfc', 'email', 'telefono', 'is_active', 'is_verified', 'inicia', 'vencimiento')
|
||||
search_fields = ('nombre', 'rfc', 'email')
|
||||
list_filter = ('is_active', 'is_verified')
|
||||
ordering = ('nombre',)
|
||||
|
||||
# class UsuarioOrganizacionAdmin(admin.ModelAdmin):
|
||||
# list_display = ('id', 'email', 'telefono', 'puesto', 'is_active', 'is_verified')
|
||||
# search_fields = ('email', 'telefono', 'puesto')
|
||||
# list_filter = ('is_active', 'is_verified')
|
||||
# ordering = ('email',)
|
||||
|
||||
admin.site.register(Organizacion)
|
||||
# admin.site.register(UsuarioOrganizacion)
|
||||
10
api/organization/apps.py
Normal file
10
api/organization/apps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
|
||||
class OrganizationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api.organization'
|
||||
|
||||
def ready(self):
|
||||
import api.organization.signals # noqa
|
||||
60
api/organization/migrations/0001_initial.py
Normal file
60
api/organization/migrations/0001_initial.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-14 16:14
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('licence', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Organizacion',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('is_agente_aduanal', models.BooleanField(default=False)),
|
||||
('nombre', models.CharField(max_length=100)),
|
||||
('rfc', models.CharField(max_length=25)),
|
||||
('titular', models.CharField(max_length=200)),
|
||||
('email', models.EmailField(max_length=100)),
|
||||
('telefono', models.CharField(max_length=25)),
|
||||
('estado', models.CharField(max_length=50)),
|
||||
('ciudad', models.CharField(max_length=50)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_verified', models.BooleanField(default=False)),
|
||||
('inicio', models.DateField(blank=True, null=True)),
|
||||
('vencimiento', models.DateField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('observaciones', models.TextField(blank=True, null=True)),
|
||||
('membretado', models.ImageField(blank=True, null=True, upload_to='membretado/')),
|
||||
('membretado_2', models.ImageField(blank=True, null=True, upload_to='membretado/')),
|
||||
('licencia', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizaciones', to='licence.licencia')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organizacion',
|
||||
'verbose_name_plural': 'Organizaciones',
|
||||
'db_table': 'organizacion',
|
||||
'ordering': ['nombre'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UsoAlmacenamiento',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('espacio_utilizado', models.PositiveBigIntegerField(default=0)),
|
||||
('organizacion', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='organization.organizacion')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Uso de Almacenamiento',
|
||||
'verbose_name_plural': 'Usos de Almacenamiento',
|
||||
'db_table': 'uso_almacenamiento',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-14 17:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('organization', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='organizacion',
|
||||
name='membretado',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizacion',
|
||||
name='membretado_2',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizacionConfiguracion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('membretado', models.ImageField(blank=True, null=True, upload_to='membretado/')),
|
||||
('membretado_2', models.ImageField(blank=True, null=True, upload_to='membretado/')),
|
||||
('organizacion', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='configuracion', to='organization.organizacion')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Configuración de Organización',
|
||||
'verbose_name_plural': 'Configuraciones de Organizaciones',
|
||||
'db_table': 'organizacion_configuracion',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
api/organization/migrations/__init__.py
Normal file
0
api/organization/migrations/__init__.py
Normal file
93
api/organization/models.py
Normal file
93
api/organization/models.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from django.db import models
|
||||
from api.licence.models import Licencia
|
||||
from django.conf import settings
|
||||
import uuid
|
||||
|
||||
class UsoAlmacenamiento(models.Model):
|
||||
organizacion = models.OneToOneField('Organizacion', on_delete=models.CASCADE)
|
||||
espacio_utilizado = models.PositiveBigIntegerField(default=0) # en bytes
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Uso de Almacenamiento"
|
||||
verbose_name_plural = "Usos de Almacenamiento"
|
||||
db_table = 'uso_almacenamiento'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.organizacion} - {self.espacio_utilizado} bytes"
|
||||
|
||||
@property
|
||||
def espacio_disponible(self):
|
||||
# Convertir GB de la licencia a bytes (1 GB = 1024^3 bytes)
|
||||
max_almacenamiento_bytes = self.organizacion.licencia.almacenamiento * 1024 ** 3
|
||||
return max_almacenamiento_bytes - self.espacio_utilizado
|
||||
|
||||
@property
|
||||
def porcentaje_utilizado(self):
|
||||
max_almacenamiento_bytes = self.organizacion.licencia.almacenamiento * 1024 ** 3
|
||||
if max_almacenamiento_bytes == 0:
|
||||
return 0
|
||||
return (self.espacio_utilizado / max_almacenamiento_bytes) * 100
|
||||
|
||||
class Organizacion(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
licencia = models.ForeignKey(Licencia, on_delete=models.CASCADE, related_name='organizaciones')
|
||||
is_agente_aduanal = models.BooleanField(default=False)
|
||||
nombre = models.CharField(max_length=100)
|
||||
rfc = models.CharField(max_length=25)
|
||||
titular = models.CharField(max_length=200)
|
||||
email = models.EmailField(max_length=100)
|
||||
telefono = models.CharField(max_length=25)
|
||||
estado = models.CharField(max_length=50)
|
||||
ciudad = models.CharField(max_length=50)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
|
||||
inicio = models.DateField(null=True, blank=True)
|
||||
vencimiento = models.DateField(null=True, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
observaciones = models.TextField(null=True, blank=True)
|
||||
|
||||
|
||||
@property
|
||||
def espacio_utilizado(self):
|
||||
uso, created = UsoAlmacenamiento.objects.get_or_create(organizacion=self)
|
||||
return uso.espacio_utilizado
|
||||
|
||||
@property
|
||||
def espacio_disponible(self):
|
||||
uso, created = UsoAlmacenamiento.objects.get_or_create(organizacion=self)
|
||||
return (self.licencia.almacenamiento * 1024 ** 3) - uso.espacio_utilizado
|
||||
|
||||
@property
|
||||
def porcentaje_utilizado(self):
|
||||
uso, created = UsoAlmacenamiento.objects.get_or_create(organizacion=self)
|
||||
if self.licencia.almacenamiento == 0:
|
||||
return 0
|
||||
return (uso.espacio_utilizado / (self.licencia.almacenamiento * 1024 ** 3)) * 100
|
||||
|
||||
def __str__(self):
|
||||
return self.nombre
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Organizacion"
|
||||
verbose_name_plural = "Organizaciones"
|
||||
db_table = 'organizacion'
|
||||
ordering = ['nombre']
|
||||
|
||||
class OrganizacionConfiguracion(models.Model):
|
||||
organizacion = models.OneToOneField(Organizacion, on_delete=models.CASCADE, related_name='configuracion')
|
||||
membretado = models.ImageField(upload_to='membretado/', null=True, blank=True)
|
||||
membretado_2 = models.ImageField(upload_to='membretado/', null=True, blank=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Configuración de Organización"
|
||||
verbose_name_plural = "Configuraciones de Organizaciones"
|
||||
db_table = 'organizacion_configuracion'
|
||||
|
||||
def __str__(self):
|
||||
return f"Configuración de {self.organizacion.nombre}"
|
||||
15
api/organization/serializers.py
Normal file
15
api/organization/serializers.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Organizacion, UsoAlmacenamiento#, UsuarioOrganizacion
|
||||
|
||||
class OrganizacionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Organizacion
|
||||
fields = '__all__'
|
||||
read_only_fields = ('created_at', 'updated_at')
|
||||
|
||||
class UsoAlmacenamientoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UsoAlmacenamiento
|
||||
fields = '__all__'
|
||||
read_only_fields = ('created_at', 'updated_at')
|
||||
8
api/organization/signals.py
Normal file
8
api/organization/signals.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from .models import Organizacion, UsoAlmacenamiento
|
||||
|
||||
@receiver(post_save, sender=Organizacion)
|
||||
def crear_uso_almacenamiento(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
UsoAlmacenamiento.objects.create(organizacion=instance, espacio_utilizado=0)
|
||||
64
api/organization/tests.py
Normal file
64
api/organization/tests.py
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Organizacion, UsoAlmacenamiento
|
||||
from api.licence.models import Licencia
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class OrganizationViewSetTests(APITestCase):
|
||||
def setUp(self):
|
||||
self.lic = Licencia.objects.create(nombre="LicTest", almacenamiento=100)
|
||||
self.org = Organizacion.objects.create(nombre="OrgTest", licencia=self.lic, is_active=True, is_verified=True)
|
||||
self.org2 = Organizacion.objects.create(nombre="OrgTest2", licencia=self.lic, is_active=True, is_verified=True)
|
||||
self.admin = User.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
|
||||
self.admin.groups.create(name="admin")
|
||||
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
|
||||
self.importador = User.objects.create_user(username="importador", password="importpass", organizacion=self.org2, is_importador=True, rfc="RFC123456789")
|
||||
self.importador.groups.create(name="importador")
|
||||
self.client = APIClient()
|
||||
|
||||
def test_admin_sees_only_own_organization(self):
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
url = reverse('Organizacion-list')
|
||||
response = self.client.get(url)
|
||||
nombres = [o['nombre'] for o in response.data]
|
||||
self.assertIn("OrgTest", nombres)
|
||||
self.assertNotIn("OrgTest2", nombres)
|
||||
|
||||
def test_superuser_sees_all_organizations(self):
|
||||
self.client.force_authenticate(user=self.superuser)
|
||||
url = reverse('Organizacion-list')
|
||||
response = self.client.get(url)
|
||||
nombres = [o['nombre'] for o in response.data]
|
||||
self.assertIn("OrgTest", nombres)
|
||||
self.assertIn("OrgTest2", nombres)
|
||||
|
||||
def test_admin_sees_only_own_storage(self):
|
||||
UsoAlmacenamiento.objects.create(organizacion=self.org, espacio_utilizado=1000)
|
||||
UsoAlmacenamiento.objects.create(organizacion=self.org2, espacio_utilizado=2000)
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
url = reverse('UsoAlmacenamiento-list')
|
||||
response = self.client.get(url)
|
||||
orgs = [u['organizacion'] for u in response.data]
|
||||
self.assertIn(self.org.id, orgs)
|
||||
self.assertNotIn(self.org2.id, orgs)
|
||||
|
||||
def test_superuser_sees_all_storage(self):
|
||||
UsoAlmacenamiento.objects.create(organizacion=self.org, espacio_utilizado=1000)
|
||||
UsoAlmacenamiento.objects.create(organizacion=self.org2, espacio_utilizado=2000)
|
||||
self.client.force_authenticate(user=self.superuser)
|
||||
url = reverse('UsoAlmacenamiento-list')
|
||||
response = self.client.get(url)
|
||||
orgs = [u['organizacion'] for u in response.data]
|
||||
self.assertIn(self.org.id, orgs)
|
||||
self.assertIn(self.org2.id, orgs)
|
||||
|
||||
def test_importador_cannot_access_storage(self):
|
||||
UsoAlmacenamiento.objects.create(organizacion=self.org2, espacio_utilizado=2000)
|
||||
self.client.force_authenticate(user=self.importador)
|
||||
url = reverse('UsoAlmacenamiento-list')
|
||||
response = self.client.get(url)
|
||||
self.assertNotEqual(response.status_code, status.HTTP_200_OK)
|
||||
25
api/organization/urls.py
Normal file
25
api/organization/urls.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# This file defines the URL patterns for the customs app in a Django project.
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
# import necessary viewsets
|
||||
# from .views import YourViewSet # Import your viewsets here
|
||||
from .views import ViewSetOrganizacion, UsoAlmacenamientoViewSet
|
||||
|
||||
# Create a router and register your viewsets with it
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
# Register your viewsets with the router here
|
||||
# Example:
|
||||
# from .views import MyViewSet
|
||||
# router.register(r'myviewset', MyViewSet, basename='myviewset')
|
||||
router.register(r'organizaciones', ViewSetOrganizacion, basename='Organizacion')
|
||||
router.register(r'uso-almacenamiento', UsoAlmacenamientoViewSet, basename='UsoAlmacenamiento')
|
||||
|
||||
#router.register(r'usuariosorganizaciones', ViewSetUsuarioOrganizacion, basename='UsuarioOrganizacion')
|
||||
# Import your viewsets here
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
122
api/organization/views.py
Normal file
122
api/organization/views.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from django.db.models import Sum
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.decorators import api_view, permission_classes, action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from api.record.models import Document
|
||||
from core.permissions import (
|
||||
IsSameOrganization,
|
||||
IsSameOrganizationDeveloper,
|
||||
IsSameOrganizationAndAdmin,
|
||||
IsSuperUser
|
||||
)
|
||||
from .serializers import OrganizacionSerializer, UsoAlmacenamientoSerializer
|
||||
from .models import Organizacion, UsoAlmacenamiento
|
||||
from api.customs.models import Pedimento
|
||||
from api.logger.mixins import LoggingMixin
|
||||
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
|
||||
|
||||
# Create your views here.
|
||||
|
||||
class ViewSetOrganizacion(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||
"""
|
||||
ViewSet for Organizacion model.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
|
||||
queryset = Organizacion.objects.all()
|
||||
serializer_class = OrganizacionSerializer
|
||||
filterset_fields = ['nombre', 'descripcion']
|
||||
|
||||
my_tags = ['Organizaciones']
|
||||
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
||||
return Organizacion.objects.none()
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
# Superuser can see all organizations
|
||||
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():
|
||||
# Importers can only see their own organization
|
||||
return Organizacion.objects.filter(users=self.request.user)
|
||||
|
||||
if self.request.user.groups.filter(name='importador').exists():
|
||||
return Organizacion.objects.filter(users=self.request.user)
|
||||
|
||||
return Organizacion.objects.none()
|
||||
|
||||
class UsoAlmacenamientoViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
Vista para consultar el uso de almacenamiento
|
||||
Solo lectura (GET) ya que la actualización se hace automáticamente
|
||||
"""
|
||||
queryset = UsoAlmacenamiento.objects.all()
|
||||
serializer_class = UsoAlmacenamientoSerializer
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
|
||||
my_tags = ['Uso de Almacenamiento']
|
||||
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
||||
return UsoAlmacenamiento.objects.none()
|
||||
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
# Superuser can see all storage usage
|
||||
return UsoAlmacenamiento.objects.all()
|
||||
|
||||
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():
|
||||
# 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():
|
||||
# Importers can only see their own organization's storage usage
|
||||
raise PermissionDenied("Los importadores no tienen acceso al uso de almacenamiento.")
|
||||
|
||||
return UsoAlmacenamiento.objects.none()
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def mi_organizacion(self, request):
|
||||
|
||||
"""Obtiene el uso de almacenamiento de la organización del usuario actual"""
|
||||
organizacion = request.user.organizacion
|
||||
|
||||
# Obtener o crear el registro de uso
|
||||
uso, created = UsoAlmacenamiento.objects.get_or_create(
|
||||
organizacion=organizacion,
|
||||
defaults={'espacio_utilizado': 0}
|
||||
)
|
||||
|
||||
# Calcular el total sumando todos los documentos (en bytes)
|
||||
total_utilizado = Document.objects.filter(
|
||||
organizacion=organizacion
|
||||
).aggregate(total=Sum('size'))['total'] or 0
|
||||
|
||||
# Sincronizar con el registro de uso (por si hay discrepancias)
|
||||
if uso.espacio_utilizado != total_utilizado:
|
||||
uso.espacio_utilizado = total_utilizado
|
||||
uso.save()
|
||||
|
||||
# Calcular métricas adicionales
|
||||
max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3
|
||||
porcentaje = (total_utilizado / max_almacenamiento_bytes * 100) if max_almacenamiento_bytes > 0 else 0
|
||||
|
||||
data = {
|
||||
'organizacion': organizacion.nombre,
|
||||
'limite_almacenamiento_gb': organizacion.licencia.almacenamiento,
|
||||
'espacio_utilizado_bytes': total_utilizado,
|
||||
'espacio_utilizado_gb': total_utilizado / (1024 ** 3),
|
||||
'espacio_disponible_bytes': max(max_almacenamiento_bytes - total_utilizado, 0),
|
||||
'porcentaje_utilizado': round(porcentaje, 2),
|
||||
'total_documentos': Document.objects.filter(organizacion=organizacion).count(),
|
||||
'total_pedimentos': Pedimento.objects.filter(organizacion=organizacion).count(),
|
||||
'total_usuarios': organizacion.users.count()
|
||||
}
|
||||
|
||||
return Response(data)
|
||||
|
||||
Reference in New Issue
Block a user