Merge pull request 'Agregando rol administrativo #31' (#35) from add_administrative_rol_#31 into main
Reviewed-on: #35
This commit is contained in:
108
AGENTS.md
Normal file
108
AGENTS.md
Normal 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
|
||||||
@@ -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,
|
||||||
|
|||||||
6
tienda_ilusion/don_confiao/permissions.py
Normal file
6
tienda_ilusion/don_confiao/permissions.py
Normal 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
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user