9 Commits

Author SHA1 Message Date
mono
079f4e3806 fix: copy application files directly into image root
- Copy tienda_ilusion contents to /app/ instead of subdirectory
- Remove volume mount from production docker-compose.yml
- Allows deployment without repository dependency

Refs #37
2026-03-30 20:45:45 -05:00
mono
6473a8348c feat: configure docker image deployment with Gitea Container Registry
- Replace local build with pre-built image from Gitea registry
- Add docker-compose.override.yml for local development with Dockerfile
- Add .dockerignore to exclude unnecessary files from build
- Update image reference to gitea.onecluster.org/oneteam/don_confiao_backend:latest

Refs #37
2026-03-30 20:34:32 -05:00
1d7beadf96 Merge pull request 'Agregar método de pago crédito #20' (#36) from add_credit_pay_method_#20 into main
Reviewed-on: #36
2026-03-14 23:46:27 -05:00
mono
5442a52ff6 feat(Payment): add CREDIT payment method 2026-03-14 23:45:52 -05:00
b852772c76 Merge pull request 'Agregando rol administrativo #31' (#35) from add_administrative_rol_#31 into main
Reviewed-on: #35
2026-03-14 18:26:31 -05:00
mono
0d5a34d366 feat: add IsAdministrator permission and protect admin endpoints 2026-03-14 16:37:44 -05:00
mono
7e2c03c81b test: add tests for user role field 2026-03-14 16:27:44 -05:00
mono
83029afd5b feat: add role field to user serializer for menu control
- Endpoint /me/ now returns 'role': 'administrator' or 'role': 'user'
- Uses Django's is_staff to determine administrator role
2026-03-07 17:58:12 -05:00
mono
2ab328b913 docs: add AGENTS.md with project context for opencode 2026-03-07 17:52:48 -05:00
12 changed files with 241 additions and 7 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.db
*.sqlite3
.env
.venv
venv/
ENV/
.git/
*.egg-info/
dist/
build/
.coverage
htmlcov/

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

@@ -5,4 +5,6 @@ WORKDIR /app/
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY tienda_ilusion/ ./
CMD ["python", "manage.py", "runserver", "0.0.0.0:9090"] CMD ["python", "manage.py", "runserver", "0.0.0.0:9090"]

View File

@@ -0,0 +1,11 @@
services:
django:
build:
context: ./
dockerfile: django.Dockerfile
volumes:
- ./tienda_ilusion:/app/
env_file:
- .env
ports:
- "7000:9090"

View File

@@ -1,12 +1,8 @@
services: services:
django: django:
build: image: gitea.onecluster.org/oneteam/don_confiao_backend:latest
context: ./
dockerfile: django.Dockerfile
env_file: env_file:
- .env - .env
volumes:
- ./tienda_ilusion:/app/
ports: ports:
- "7000:9090" - "7000:9090"

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,23 @@
# Generated by Django 5.0.6 on 2026-03-15 04:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0043_customer_address_external_id'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='type_payment',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia'), ('CREDIT', 'Crédito')], default='CASH', max_length=30),
),
migrations.AlterField(
model_name='sale',
name='payment_method',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia'), ('CREDIT', 'Crédito')], default='CASH', max_length=30),
),
]

View File

@@ -10,6 +10,7 @@ class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Efectivo') CASH = 'CASH', _('Efectivo')
CONFIAR = 'CONFIAR', _('Confiar') CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia') BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
CREDIT = 'CREDIT', _('Crédito')
class Customer(models.Model): class Customer(models.Model):

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

@@ -21,3 +21,4 @@ class TestPaymentMethods(TestCase, LoginMixin):
self.assertIn('CASH', [method.get('value') for method in methods]) self.assertIn('CASH', [method.get('value') for method in methods])
self.assertIn('CONFIAR', [method.get('value') for method in methods]) self.assertIn('CONFIAR', [method.get('value') for method in methods])
self.assertIn('BANCOLOMBIA', [method.get('value') for method in methods]) self.assertIn('BANCOLOMBIA', [method.get('value') for method in methods])
self.assertIn('CREDIT', [method.get('value') for method in methods])

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):
""" """