Merge pull request 'Agregando rol administrativo #31' (#35) from add_administrative_rol_#31 into main

Reviewed-on: #35
This commit is contained in:
2026-03-14 18:26:31 -05:00
5 changed files with 185 additions and 2 deletions

108
AGENTS.md Normal file
View File

@@ -0,0 +1,108 @@
# Don Confiao Backend - Contexto del Proyecto
## Tipo de Proyecto
Backend Django con Django REST Framework
## Estructura del Proyecto
```
don_confiao_backend/
├── requirements.txt # Dependencias Python
├── docker-compose.yml # Configuración Docker
├── django.Dockerfile # Dockerfile Django
├── .env # Variables de entorno
├── .env_example # Ejemplo de variables de entorno
├── README.rst # Documentación básica
├── Rakefile # Tareas rake
├── doc/ # Documentación adicional
│ └── requests.org
└── tienda_ilusion/ # Proyecto Django
├── manage.py
├── db.sqlite3 # Base de datos SQLite
├── don_confiao/ # App principal
│ ├── models.py # Modelos: Customer, Product, Sale, SaleLine, Payment, ReconciliationJar, AdminCode
│ ├── views.py
│ ├── api_views.py
│ ├── serializers.py
│ ├── forms.py
│ ├── admin.py
│ ├── urls.py
│ ├── export_csv.py
│ ├── tests/ # Tests
│ └── migrations/
├── users/ # App de usuarios
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ └── tests/
└── tienda_ilusion/ # Configuración Django
├── settings.py
├── urls.py
├── wsgi.py
└── asgi.py
```
## Dependencias Principales
- Django==5.0.6
- djangorestframework
- django-cors-headers
- djangorestframework-simplejwt
- sabatron-tryton-rpc-client==7.4.0 (integración con Tryton ERP)
## Modelos Principales (don_confiao/models.py)
- **Customer**: Clientes (name, address, email, phone, external_id)
- **Product**: Productos (name, price, measuring_unit, categories)
- **ProductCategory**: Categorías de productos
- **Sale**: Ventas (customer, date, phone, description, payment_method, reconciliation)
- **SaleLine**: Líneas de venta (sale, product, quantity, unit_price, description)
- **Payment**: Pagos (date_time, type_payment, amount, reconciliation_jar)
- **PaymentSale**: Relación muchos a muchos entre Payment y Sale
- **ReconciliationJar**: Arqueo de caja (is_valid, date_time, reconcilier, cash_taken, cash_discrepancy)
- **AdminCode**: Códigos de administrador
## Autenticación
- JWT con djangorestframework-simplejwt
- ACCESS_TOKEN_LIFETIME: 30 minutos
- REFRESH_TOKEN_LIFETIME: 1 día
## API Endpoints
- REST API en don_confiao/api_views.py y users/
- URLs en don_confiao/urls.py y users/urls.py
## Ejecución con Docker Compose
El proyecto se ejecuta con docker-compose. Todos los comandos `manage.py` deben ejecutarse dentro del contenedor:
```bash
# Ejecutar tests
docker-compose run --rm django python manage.py test
# Migraciones
docker-compose run --rm django python manage.py makemigrations
docker-compose run --rm django python manage.py migrate
# Servidor desarrollo
docker-compose up
# Shell Django
docker-compose run --rm django python manage.py shell
# Crear superuser
docker-compose run --rm django python manage.py createsuperuser
```
Nota: El volumen monta `tienda_ilusion/` en `/app/`, por lo que el path correcto es `python manage.py` (no `python tienda_ilusion/manage.py`).
## Tests
- Framework: Django unittest
- Directorio: don_confiao/tests/
- Ejecutar: `docker-compose run --rm django python manage.py test`
## Comandos Útiles (dentro del contenedor)
- Migraciones: `docker-compose run --rm django python manage.py makemigrations && docker-compose run --rm django python manage.py migrate`
- Servidor desarrollo: `docker-compose up`
- Shell Django: `docker-compose run --rm django python manage.py shell`
- Superuser: `docker-compose run --rm django python manage.py createsuperuser`
## Integraciones
- **Tryton ERP**: Integración mediante sabatron-tryton-rpc-client para sincronización de clientes, productos y ventas

View File

@@ -3,10 +3,12 @@ from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
from .views import sales_to_tryton_csv from .views import sales_to_tryton_csv
from .permissions import IsAdministrator
from decimal import Decimal from decimal import Decimal
from sabatron_tryton_rpc_client.client import Client from sabatron_tryton_rpc_client.client import Client
@@ -74,6 +76,8 @@ class CustomerView(viewsets.ModelViewSet):
class ReconciliateJarView(APIView): class ReconciliateJarView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request): def post(self, request):
data = request.data data = request.data
cash_purchases_id = data.get('cash_purchases') cash_purchases_id = data.get('cash_purchases')
@@ -131,6 +135,8 @@ class PaymentMethodView(APIView):
class SalesForReconciliationView(APIView): class SalesForReconciliationView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request): def get(self, request):
sales = Sale.objects.filter(reconciliation=None) sales = Sale.objects.filter(reconciliation=None)
grouped_sales = {} grouped_sales = {}
@@ -152,6 +158,8 @@ class SaleSummary(APIView):
class AdminCodeValidateView(APIView): class AdminCodeValidateView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request, code): def get(self, request, code):
codes = AdminCode.objects.filter(value=code) codes = AdminCode.objects.filter(value=code)
return Response({'validCode': bool(codes)}) return Response({'validCode': bool(codes)})
@@ -161,9 +169,12 @@ class ReconciliateJarModelView(viewsets.ModelViewSet):
queryset = ReconciliationJar.objects.all().order_by('-date_time') queryset = ReconciliationJar.objects.all().order_by('-date_time')
pagination_class = Pagination pagination_class = Pagination
serializer_class = ReconciliationJarSerializer serializer_class = ReconciliationJarSerializer
permission_classes = [IsAuthenticated, IsAdministrator]
class SalesForTrytonView(APIView): class SalesForTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request): def get(self, request):
sales = Sale.objects.all() sales = Sale.objects.all()
csv = self._generate_sales_CSV(sales) csv = self._generate_sales_CSV(sales)
@@ -180,6 +191,8 @@ class SalesForTrytonView(APIView):
class SalesToTrytonView(APIView): class SalesToTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request): def post(self, request):
tryton_client = Client( tryton_client = Client(
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
@@ -269,6 +282,8 @@ class TrytonLineSale:
class ProductsFromTrytonView(APIView): class ProductsFromTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request): def post(self, request):
tryton_client = Client( tryton_client = Client(
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
@@ -362,6 +377,8 @@ class ProductsFromTrytonView(APIView):
class CustomersFromTrytonView(APIView): class CustomersFromTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request): def post(self, request):
tryton_client = Client( tryton_client = Client(
hostname=TRYTON_HOST, hostname=TRYTON_HOST,

View File

@@ -0,0 +1,6 @@
from rest_framework.permissions import BasePermission
class IsAdministrator(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_staff

View File

@@ -3,6 +3,11 @@ from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
role = serializers.SerializerMethodField()
class Meta: class Meta:
model = User model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name') fields = ('id', 'username', 'email', 'first_name', 'last_name', 'role')
def get_role(self, obj):
return 'administrator' if obj.is_staff else 'user'

View File

@@ -31,7 +31,7 @@ class MeEndpointTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_fields = {'id', 'username', 'email', expected_fields = {'id', 'username', 'email',
'first_name', 'last_name'} 'first_name', 'last_name', 'role'}
self.assertTrue(expected_fields.issubset(response.json().keys())) self.assertTrue(expected_fields.issubset(response.json().keys()))
data = response.json() data = response.json()
@@ -39,6 +39,53 @@ class MeEndpointTests(TestCase):
self.assertEqual(data['email'], self.user.email) self.assertEqual(data['email'], self.user.email)
self.assertEqual(data['first_name'], self.user.first_name) self.assertEqual(data['first_name'], self.user.first_name)
self.assertEqual(data['last_name'], self.user.last_name) self.assertEqual(data['last_name'], self.user.last_name)
self.assertEqual(data['role'], 'administrator')
def test_regular_user_role_is_user(self):
"""
Verifica que un usuario sin permisos de staff recibe role 'user'.
"""
regular_user = User.objects.create_user(
username='regular',
email='regular@example.com',
password='regularpass',
is_staff=False
)
refresh = RefreshToken.for_user(regular_user)
access_token = str(refresh.access_token)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
url = reverse('current-user')
response = client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['role'], 'user')
def test_staff_user_role_is_administrator(self):
"""
Verifica que un usuario con is_staff=True recibe role 'administrator'.
"""
staff_user = User.objects.create_user(
username='staff',
email='staff@example.com',
password='staffpass',
is_staff=True
)
refresh = RefreshToken.for_user(staff_user)
access_token = str(refresh.access_token)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
url = reverse('current-user')
response = client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['role'], 'administrator')
def test_me_endpoint_requires_authentication(self): def test_me_endpoint_requires_authentication(self):
""" """