diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..52d6f4d --- /dev/null +++ b/AGENTS.md @@ -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 diff --git a/tienda_ilusion/don_confiao/api_views.py b/tienda_ilusion/don_confiao/api_views.py index f033e1c..5b7509f 100644 --- a/tienda_ilusion/don_confiao/api_views.py +++ b/tienda_ilusion/don_confiao/api_views.py @@ -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, diff --git a/tienda_ilusion/don_confiao/permissions.py b/tienda_ilusion/don_confiao/permissions.py new file mode 100644 index 0000000..d36562a --- /dev/null +++ b/tienda_ilusion/don_confiao/permissions.py @@ -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 diff --git a/tienda_ilusion/users/serializers.py b/tienda_ilusion/users/serializers.py index cdc3387..1a09ef3 100644 --- a/tienda_ilusion/users/serializers.py +++ b/tienda_ilusion/users/serializers.py @@ -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' diff --git a/tienda_ilusion/users/tests.py b/tienda_ilusion/users/tests.py index ad750ad..5b4da6d 100644 --- a/tienda_ilusion/users/tests.py +++ b/tienda_ilusion/users/tests.py @@ -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): """