Agregando rol administrativo #31 #35
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.views import APIView
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
|
||||
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
|
||||
from .views import sales_to_tryton_csv
|
||||
from .permissions import IsAdministrator
|
||||
|
||||
from decimal import Decimal
|
||||
from sabatron_tryton_rpc_client.client import Client
|
||||
@@ -74,6 +76,8 @@ class CustomerView(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class ReconciliateJarView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
cash_purchases_id = data.get('cash_purchases')
|
||||
@@ -131,6 +135,8 @@ class PaymentMethodView(APIView):
|
||||
|
||||
|
||||
class SalesForReconciliationView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def get(self, request):
|
||||
sales = Sale.objects.filter(reconciliation=None)
|
||||
grouped_sales = {}
|
||||
@@ -152,6 +158,8 @@ class SaleSummary(APIView):
|
||||
|
||||
|
||||
class AdminCodeValidateView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def get(self, request, code):
|
||||
codes = AdminCode.objects.filter(value=code)
|
||||
return Response({'validCode': bool(codes)})
|
||||
@@ -161,9 +169,12 @@ class ReconciliateJarModelView(viewsets.ModelViewSet):
|
||||
queryset = ReconciliationJar.objects.all().order_by('-date_time')
|
||||
pagination_class = Pagination
|
||||
serializer_class = ReconciliationJarSerializer
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
|
||||
class SalesForTrytonView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def get(self, request):
|
||||
sales = Sale.objects.all()
|
||||
csv = self._generate_sales_CSV(sales)
|
||||
@@ -180,6 +191,8 @@ class SalesForTrytonView(APIView):
|
||||
|
||||
|
||||
class SalesToTrytonView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def post(self, request):
|
||||
tryton_client = Client(
|
||||
hostname=TRYTON_HOST,
|
||||
@@ -269,6 +282,8 @@ class TrytonLineSale:
|
||||
|
||||
|
||||
class ProductsFromTrytonView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def post(self, request):
|
||||
tryton_client = Client(
|
||||
hostname=TRYTON_HOST,
|
||||
@@ -362,6 +377,8 @@ class ProductsFromTrytonView(APIView):
|
||||
|
||||
|
||||
class CustomersFromTrytonView(APIView):
|
||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||
|
||||
def post(self, request):
|
||||
tryton_client = Client(
|
||||
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):
|
||||
role = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
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)
|
||||
|
||||
expected_fields = {'id', 'username', 'email',
|
||||
'first_name', 'last_name'}
|
||||
'first_name', 'last_name', 'role'}
|
||||
self.assertTrue(expected_fields.issubset(response.json().keys()))
|
||||
|
||||
data = response.json()
|
||||
@@ -39,6 +39,53 @@ class MeEndpointTests(TestCase):
|
||||
self.assertEqual(data['email'], self.user.email)
|
||||
self.assertEqual(data['first_name'], self.user.first_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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user