Mudanza de repo

This commit is contained in:
2025-09-22 18:43:29 -06:00
parent 26fe36ca52
commit d11d543bdc
193 changed files with 10998 additions and 0 deletions

View File

18
api/organization/admin.py Normal file
View 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
View 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

View 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',
},
),
]

View File

@@ -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',
},
),
]

View File

View 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}"

View 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')

View 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
View 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
View 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
View 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)