2 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
70 changed files with 1676 additions and 3415 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/

View File

@@ -1,26 +0,0 @@
# Development Environment Variables
# Este archivo contiene las variables de entorno para desarrollo local
# Django Environment
DJANGO_ENV=development
# Debug mode (True for development)
DEBUG=True
# Django Secret Key (insecure key for development only)
SECRET_KEY=django-insecure-development-key-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v
# Allowed hosts (comma-separated)
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# CORS allowed origins (comma-separated)
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:7001,http://localhost:5173
# Database (SQLite by default in development)
# No additional DB configuration needed for SQLite
# Tryton ERP Configuration
TRYTON_HOST=recreo.onecluster.com.co
TRYTON_DATABASE=ilusion_staging
TRYTON_USERNAME=alejandro.ayala
TRYTON_PASSWORD=cl4v3alejo

View File

@@ -1,56 +0,0 @@
# Production Environment Variables - EXAMPLE
# ¡IMPORTANTE! Copia este archivo a .env.production y completa con valores reales
# NO commitees .env.production con valores sensibles a git
# Django Environment
DJANGO_ENV=production
# Debug mode (MUST be False in production)
DEBUG=False
# Django Secret Key
# ¡IMPORTANTE! Genera una clave única y segura para producción
# Puedes generar una con: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
SECRET_KEY=CHANGE-ME-TO-A-SECURE-RANDOM-SECRET-KEY-IN-PRODUCTION
# Allowed hosts (comma-separated domains)
# Ejemplo: ALLOWED_HOSTS=tiendailusion.com,www.tiendailusion.com,api.tiendailusion.com
ALLOWED_HOSTS=tiendailusion.com,www.tiendailusion.com
# CORS allowed origins (comma-separated URLs)
# Ejemplo: CORS_ALLOWED_ORIGINS=https://tiendailusion.com,https://www.tiendailusion.com
CORS_ALLOWED_ORIGINS=https://tiendailusion.com,https://www.tiendailusion.com
# CSRF Trusted Origins (comma-separated URLs)
# Debe incluir el protocolo (https://)
CSRF_TRUSTED_ORIGINS=https://tiendailusion.com,https://www.tiendailusion.com
# PostgreSQL Database Configuration
DB_NAME=tienda_ilusion_prod
DB_USER=tienda_ilusion_user
DB_PASSWORD=CHANGE-ME-TO-A-SECURE-DATABASE-PASSWORD
DB_HOST=postgres
DB_PORT=5432
# Email Configuration (para notificaciones y recuperación de contraseñas)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=noreply@tiendailusion.com
EMAIL_HOST_PASSWORD=CHANGE-ME-TO-YOUR-EMAIL-PASSWORD
DEFAULT_FROM_EMAIL=noreply@tiendailusion.com
# Admin notifications
ADMIN_EMAIL=admin@tiendailusion.com
# Tryton ERP Configuration (Production)
TRYTON_HOST=tryton-production-server
TRYTON_DATABASE=tryton_production
TRYTON_USERNAME=tienda_ilusion_integration
TRYTON_PASSWORD=CHANGE-ME-TO-TRYTON-PASSWORD
# Optional: Redis URL for caching (if using Redis)
# REDIS_URL=redis://redis:6379/1
# Optional: Sentry DSN for error tracking
# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id

View File

@@ -1,51 +0,0 @@
# Staging Environment Variables
# Testing de producción en localhost sin SSL
# Este ambiente simula producción pero permite HTTP en localhost
# Django Environment
DJANGO_ENV=staging
# Debug mode (False para simular producción)
DEBUG=False
# Django Secret Key
# Para staging local, puedes usar una key fija (NO usar en producción real)
SECRET_KEY=staging-local-key-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v
# Allowed hosts (localhost para testing)
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# CORS allowed origins (permite acceso desde navegador local)
# Incluye común puertos de desarrollo de frontend y acceso directo
CORS_ALLOWED_ORIGINS=http://localhost:8000,http://localhost:3000,http://localhost:5173,http://localhost:7001,http://127.0.0.1:8000
# CSRF Trusted Origins (localhost HTTP para staging)
CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
# PostgreSQL Database Configuration (staging local)
DB_NAME=tienda_ilusion_staging
DB_USER=tienda_ilusion_user
DB_PASSWORD=staging_local_password
DB_HOST=postgres
DB_PORT=5432
# Email Configuration (console backend para staging)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=staging@tiendailusion.local
EMAIL_HOST_PASSWORD=staging_password
DEFAULT_FROM_EMAIL=staging@tiendailusion.local
# Admin notifications
ADMIN_EMAIL=admin@tiendailusion.local
# Tryton ERP Configuration (Staging)
# Ajusta estos valores según tu entorno de staging de Tryton
TRYTON_HOST=localhost
TRYTON_DATABASE=tryton_staging
TRYTON_USERNAME=admin
TRYTON_PASSWORD=admin
# Staging specific: Disable SSL redirect for localhost testing
STAGING_DISABLE_SSL=True

4
.env_example Normal file
View File

@@ -0,0 +1,4 @@
TRYTON_HOST=localhost
TRYTON_DATABASE=tryton
TRYTON_USERNAME=admin
TRYTON_PASSWORD=admin

31
.gitignore vendored
View File

@@ -327,40 +327,9 @@ pip-selfcheck.json
# End of https://www.toptal.com/developers/gitignore/api/emacs,python,django,venv # End of https://www.toptal.com/developers/gitignore/api/emacs,python,django,venv
# Project-specific ignores
/tienda_ilusion/don_confiao/static/frontend/ /tienda_ilusion/don_confiao/static/frontend/
/tienda_ilusion/don_confiao/frontend/don-confiao/.vite/ /tienda_ilusion/don_confiao/frontend/don-confiao/.vite/
/tienda_ilusion/don_confiao/frontend/don-confiao/.eslintrc.js /tienda_ilusion/don_confiao/frontend/don-confiao/.eslintrc.js
/tienda_ilusion/don_confiao/frontend/don-confiao/.eslintrc-auto-import.json /tienda_ilusion/don_confiao/frontend/don-confiao/.eslintrc-auto-import.json
/tienda_ilusion/don_confiao/frontend/don-confiao/.editorconfig /tienda_ilusion/don_confiao/frontend/don-confiao/.editorconfig
/tienda_ilusion/don_confiao/frontend/don-confiao/.browserslistrc /tienda_ilusion/don_confiao/frontend/don-confiao/.browserslistrc
# Environment files with sensitive data
.env.production
.env.local
.env.*.local
# Static files collected by Django
/tienda_ilusion/staticfiles/
staticfiles/
# Media files uploaded by users
/tienda_ilusion/media/
# Application logs
/tienda_ilusion/logs/
*.log
# Database backups
backups/
*.sql
*.dump
# Docker volumes and data
postgres_data/
pgdata/
# IDE-specific
.vscode/
.idea/
.env.staging

377
AGENTS.md
View File

@@ -7,23 +7,19 @@ Backend Django con Django REST Framework
``` ```
don_confiao_backend/ don_confiao_backend/
├── requirements.txt # Dependencias Python ├── requirements.txt # Dependencias Python
├── docker-compose.dev.yml # Docker Compose para desarrollo ├── docker-compose.yml # Configuración Docker
├── docker-compose.staging.yml # Docker Compose para staging (testing producción local) ├── django.Dockerfile # Dockerfile Django
├── docker-compose.prod.yml # Docker Compose para producción ├── .env # Variables de entorno
├── django.Dockerfile # Dockerfile Django ├── .env_example # Ejemplo de variables de entorno
├── .env.development # Variables de entorno desarrollo ├── README.rst # Documentación básica
├── .env.staging # Variables de entorno staging ├── Rakefile # Tareas rake
├── .env.production.example # Ejemplo variables de entorno producción ├── doc/ # Documentación adicional
├── .env_example # Ejemplo de variables de entorno
├── README.rst # Documentación básica
├── Rakefile # Tareas rake
├── doc/ # Documentación adicional
│ └── requests.org │ └── requests.org
└── tienda_ilusion/ # Proyecto Django └── tienda_ilusion/ # Proyecto Django
├── manage.py ├── manage.py
├── db.sqlite3 # Base de datos SQLite (desarrollo) ├── db.sqlite3 # Base de datos SQLite
├── don_confiao/ # App principal ├── don_confiao/ # App principal
│ ├── models.py # Modelos: Customer, Product, Sale, SaleLine, Payment, ReconciliationJar, AdminCode │ ├── models.py # Modelos: Customer, Product, Sale, SaleLine, Payment, ReconciliationJar, AdminCode
│ ├── views.py │ ├── views.py
│ ├── api_views.py │ ├── api_views.py
│ ├── serializers.py │ ├── serializers.py
@@ -31,21 +27,16 @@ don_confiao_backend/
│ ├── admin.py │ ├── admin.py
│ ├── urls.py │ ├── urls.py
│ ├── export_csv.py │ ├── export_csv.py
│ ├── tests/ # Tests │ ├── tests/ # Tests
│ └── migrations/ │ └── migrations/
├── users/ # App de usuarios ├── users/ # App de usuarios
│ ├── models.py │ ├── models.py
│ ├── views.py │ ├── views.py
│ ├── serializers.py │ ├── serializers.py
│ ├── urls.py │ ├── urls.py
│ └── tests/ │ └── tests/
└── config/ # Configuración Django └── tienda_ilusion/ # Configuración Django
├── settings/ # Settings por ambiente ├── settings.py
│ ├── __init__.py # Detección automática de ambiente
│ ├── base.py # Configuración compartida
│ ├── development.py # Configuración desarrollo
│ ├── staging.py # Configuración staging
│ └── production.py # Configuración producción
├── urls.py ├── urls.py
├── wsgi.py ├── wsgi.py
└── asgi.py └── asgi.py
@@ -57,9 +48,6 @@ don_confiao_backend/
- django-cors-headers - django-cors-headers
- djangorestframework-simplejwt - djangorestframework-simplejwt
- sabatron-tryton-rpc-client==7.4.0 (integración con Tryton ERP) - sabatron-tryton-rpc-client==7.4.0 (integración con Tryton ERP)
- psycopg2-binary (driver PostgreSQL para producción)
- gunicorn (servidor WSGI para producción)
- python-decouple (gestión de variables de entorno)
## Modelos Principales (don_confiao/models.py) ## Modelos Principales (don_confiao/models.py)
- **Customer**: Clientes (name, address, email, phone, external_id) - **Customer**: Clientes (name, address, email, phone, external_id)
@@ -85,73 +73,22 @@ don_confiao_backend/
El proyecto se ejecuta con docker-compose. Todos los comandos `manage.py` deben ejecutarse dentro del contenedor: El proyecto se ejecuta con docker-compose. Todos los comandos `manage.py` deben ejecutarse dentro del contenedor:
### Desarrollo (Development)
```bash ```bash
# Ejecutar tests # Ejecutar tests
docker compose -f docker-compose.dev.yml run --rm django python manage.py test docker-compose run --rm django python manage.py test
# Migraciones # Migraciones
docker compose -f docker-compose.dev.yml run --rm django python manage.py makemigrations docker-compose run --rm django python manage.py makemigrations
docker compose -f docker-compose.dev.yml run --rm django python manage.py migrate docker-compose run --rm django python manage.py migrate
# Servidor desarrollo # Servidor desarrollo
docker compose -f docker-compose.dev.yml up docker-compose up
# Shell Django # Shell Django
docker compose -f docker-compose.dev.yml run --rm django python manage.py shell docker-compose run --rm django python manage.py shell
# Crear superuser # Crear superuser
docker compose -f docker-compose.dev.yml run --rm django python manage.py createsuperuser docker-compose run --rm django python manage.py createsuperuser
```
### Staging (Testing Producción Local)
```bash
# Iniciar servicios (PostgreSQL + Django con Gunicorn)
# Ya incluye el archivo .env.staging, listo para usar
docker compose -f docker-compose.staging.yml up -d
# Migraciones
docker compose -f docker-compose.staging.yml run --rm django python manage.py migrate
# Colectar archivos estáticos
docker compose -f docker-compose.staging.yml run --rm django python manage.py collectstatic --noinput
# Crear superuser
docker compose -f docker-compose.staging.yml run --rm django python manage.py createsuperuser
# Ver logs
docker compose -f docker-compose.staging.yml logs -f django
# Detener servicios
docker compose -f docker-compose.staging.yml down
```
### Producción (Production)
```bash
# Primero, copiar .env.production.example a .env.production y configurar variables
cp .env.production.example .env.production
# Editar .env.production con valores reales
# Iniciar servicios (PostgreSQL + Django con Gunicorn)
docker compose -f docker-compose.prod.yml up -d
# Migraciones
docker compose -f docker-compose.prod.yml run --rm django python manage.py migrate
# Colectar archivos estáticos
docker compose -f docker-compose.prod.yml run --rm django python manage.py collectstatic --noinput
# Crear superuser
docker compose -f docker-compose.prod.yml run --rm django python manage.py createsuperuser
# Ver logs
docker compose -f docker-compose.prod.yml logs -f django
# Detener servicios
docker compose -f docker-compose.prod.yml down
``` ```
Nota: El volumen monta `tienda_ilusion/` en `/app/`, por lo que el path correcto es `python manage.py` (no `python tienda_ilusion/manage.py`). Nota: El volumen monta `tienda_ilusion/` en `/app/`, por lo que el path correcto es `python manage.py` (no `python tienda_ilusion/manage.py`).
@@ -159,275 +96,13 @@ Nota: El volumen monta `tienda_ilusion/` en `/app/`, por lo que el path correcto
## Tests ## Tests
- Framework: Django unittest - Framework: Django unittest
- Directorio: don_confiao/tests/ - Directorio: don_confiao/tests/
- Ejecutar: `docker-compose -f docker-compose.dev.yml run --rm django python manage.py test` - Ejecutar: `docker-compose run --rm django python manage.py test`
## Comandos Útiles (dentro del contenedor) ## Comandos Útiles (dentro del contenedor)
- Migraciones: `docker-compose -f docker-compose.dev.yml run --rm django python manage.py makemigrations && docker-compose -f docker-compose.dev.yml run --rm django python manage.py migrate` - Migraciones: `docker-compose run --rm django python manage.py makemigrations && docker-compose run --rm django python manage.py migrate`
- Servidor desarrollo: `docker-compose -f docker-compose.dev.yml up` - Servidor desarrollo: `docker-compose up`
- Shell Django: `docker-compose -f docker-compose.dev.yml run --rm django python manage.py shell` - Shell Django: `docker-compose run --rm django python manage.py shell`
- Superuser: `docker-compose -f docker-compose.dev.yml run --rm django python manage.py createsuperuser` - Superuser: `docker-compose run --rm django python manage.py createsuperuser`
## Configuración de Ambientes
El proyecto soporta tres ambientes diferentes mediante la variable `DJANGO_ENV`:
### Development (desarrollo local)
- **Variable de ambiente**: `DJANGO_ENV=development`
- **Archivo de configuración**: `.env.development`
- **Base de datos**: SQLite (db.sqlite3)
- **DEBUG**: True
- **CORS**: Permisivo para desarrollo local
- **Docker Compose**: `docker-compose.dev.yml`
### Staging (testing de producción local)
- **Variable de ambiente**: `DJANGO_ENV=staging`
- **Archivo de configuración**: `.env.staging`
- **Base de datos**: PostgreSQL (tienda_ilusion_staging)
- **DEBUG**: False (simula producción)
- **CORS**: Permisivo para localhost (sin SSL redirect)
- **Docker Compose**: `docker-compose.staging.yml`
- **Servidor**: Gunicorn con 4 workers
- **Puerto PostgreSQL**: 5433 (para no conflictuar con producción)
### Production (producción)
- **Variable de ambiente**: `DJANGO_ENV=production`
- **Archivo de configuración**: `.env.production` (crear desde `.env.production.example`)
- **Base de datos**: PostgreSQL
- **DEBUG**: False
- **Seguridad**: HTTPS, HSTS, secure cookies, CSRF protections
- **Docker Compose**: `docker-compose.prod.yml`
- **Servidor**: Gunicorn con 4 workers
### Cambiar entre ambientes
La detección de ambiente es automática mediante la variable `DJANGO_ENV`. Docker Compose configura esta variable automáticamente según el archivo usado:
- `docker-compose.dev.yml` → usa `.env.development` → carga `settings/development.py`
- `docker-compose.staging.yml` → usa `.env.staging` → carga `settings/staging.py`
- `docker-compose.prod.yml` → usa `.env.production` → carga `settings/production.py`
### Variables de entorno requeridas
**Desarrollo** (`.env.development`):
- `DJANGO_ENV=development`
- `DEBUG=True`
- `TRYTON_HOST`, `TRYTON_DATABASE`, `TRYTON_USERNAME`, `TRYTON_PASSWORD`
**Staging** (`.env.staging`):
- `DJANGO_ENV=staging`
- `DEBUG=False`
- `SECRET_KEY` (para staging local, puede ser una key fija)
- `ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0`
- `CORS_ALLOWED_ORIGINS` (URLs localhost separadas por comas)
- `DB_NAME=tienda_ilusion_staging`, `DB_USER`, `DB_PASSWORD`, `DB_HOST=postgres`, `DB_PORT=5432`
- `TRYTON_HOST`, `TRYTON_DATABASE`, `TRYTON_USERNAME`, `TRYTON_PASSWORD`
**Producción** (`.env.production`):
- `DJANGO_ENV=production`
- `DEBUG=False`
- `SECRET_KEY` (generar una nueva y segura)
- `ALLOWED_HOSTS` (dominios separados por comas)
- `CORS_ALLOWED_ORIGINS` (URLs separadas por comas)
- `CSRF_TRUSTED_ORIGINS` (URLs separadas por comas)
- `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`
- `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD`
- `TRYTON_HOST`, `TRYTON_DATABASE`, `TRYTON_USERNAME`, `TRYTON_PASSWORD`
Ver `.env.production.example` para todos los detalles.
## Scripts Útiles
El proyecto incluye scripts para facilitar operaciones comunes:
### Health Check
Verifica el estado de todos los servicios:
```bash
# Verificar ambiente de desarrollo
./scripts/health-check.sh dev
# Verificar ambiente de producción
./scripts/health-check.sh prod
```
### Backup de Base de Datos (Producción)
Crea un backup comprimido de la base de datos PostgreSQL:
```bash
./scripts/backup-db.sh
```
Los backups se guardan en `backups/` y se mantienen por 7 días automáticamente.
### Restore de Base de Datos (Producción)
Restaura un backup de la base de datos:
```bash
./scripts/restore-backup.sh <archivo_backup.sql.gz>
```
**ADVERTENCIA**: Esto reemplazará la base de datos actual. Se crea un backup de seguridad antes de restaurar.
## Troubleshooting
### Errores Comunes
#### 1. Error: "database 'tienda_ilusion_user' does not exist"
**Síntoma**: Aparece en los logs de PostgreSQL
```
FATAL: database "tienda_ilusion_user" does not exist
```
**Causa**: Este es un comportamiento normal de PostgreSQL. Cuando te conectas sin especificar una base de datos, PostgreSQL intenta conectarse a una base con el mismo nombre del usuario.
**Solución**: Este error NO afecta el funcionamiento de Django. Para verificar que la base de datos correcta existe:
```bash
docker exec tienda_ilusion_postgres_prod psql -U tienda_ilusion_user -d tienda_ilusion_prod -c '\l'
```
#### 2. Error: "required variable DB_PASSWORD is missing a value"
**Síntoma**: Docker Compose falla al iniciar con error de variable faltante
**Causa**: El archivo `.env.production` no existe o tiene valores placeholder
**Solución**:
1. Copiar el archivo de ejemplo: `cp .env.production.example .env.production`
2. Editar `.env.production` y reemplazar todos los valores `CHANGE-ME-...` con valores reales
3. Generar SECRET_KEY segura:
```bash
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
```
#### 3. Error: "ModuleNotFoundError" o importación fallida
**Síntoma**: Django no puede importar módulos después de cambiar configuración
**Causa**: Dependencias no instaladas o contenedor con caché viejo
**Solución**:
```bash
# Reconstruir contenedores
docker-compose -f docker-compose.dev.yml build --no-cache
docker-compose -f docker-compose.dev.yml up
# Para producción
docker-compose -f docker-compose.prod.yml build --no-cache
docker-compose -f docker-compose.prod.yml up -d
```
#### 4. Error: "django.db.utils.OperationalError: could not connect to server"
**Síntoma**: Django no puede conectarse a PostgreSQL
**Causa**: PostgreSQL aún no está listo cuando Django intenta conectar
**Solución**: El healthcheck en `docker-compose.prod.yml` maneja esto automáticamente. Si el problema persiste:
```bash
# Verificar que PostgreSQL esté corriendo
docker ps | grep postgres
# Ver logs de PostgreSQL
docker logs tienda_ilusion_postgres_prod
# Reiniciar servicios en orden
docker-compose -f docker-compose.prod.yml restart postgres
docker-compose -f docker-compose.prod.yml restart django
```
#### 5. Error: "CSRF verification failed"
**Síntoma**: Errores CSRF en producción
**Causa**: `CSRF_TRUSTED_ORIGINS` no configurado correctamente
**Solución**: En `.env.production`, asegurar que `CSRF_TRUSTED_ORIGINS` incluya el protocolo:
```bash
CSRF_TRUSTED_ORIGINS=https://tudominio.com,https://www.tudominio.com
```
#### 6. Static files no se sirven en producción
**Síntoma**: CSS/JS no cargan, errores 404 para archivos estáticos
**Causa**: `collectstatic` no ejecutado o configuración de nginx incorrecta
**Solución**:
```bash
# Ejecutar collectstatic
docker-compose -f docker-compose.prod.yml exec django python manage.py collectstatic --noinput
# Verificar que los archivos existen
docker exec tienda_ilusion_django_prod ls -la /app/staticfiles
```
#### 7. Migraciones pendientes después de deployment
**Síntoma**: Errores de base de datos o tablas faltantes
**Causa**: Migraciones no aplicadas en producción
**Solución**:
```bash
# Ver estado de migraciones
docker-compose -f docker-compose.prod.yml exec django python manage.py showmigrations
# Aplicar migraciones pendientes
docker-compose -f docker-compose.prod.yml exec django python manage.py migrate
```
### Comandos de Diagnóstico
```bash
# Ver todos los contenedores
docker ps -a
# Ver logs en tiempo real
docker-compose -f docker-compose.prod.yml logs -f
# Ver logs solo de Django
docker logs -f tienda_ilusion_django_prod
# Ver logs solo de PostgreSQL
docker logs -f tienda_ilusion_postgres_prod
# Ejecutar shell en contenedor Django
docker-compose -f docker-compose.prod.yml exec django bash
# Ejecutar Django shell
docker-compose -f docker-compose.prod.yml exec django python manage.py shell
# Conectar a PostgreSQL
docker exec -it tienda_ilusion_postgres_prod psql -U tienda_ilusion_user -d tienda_ilusion_prod
# Verificar variables de entorno en contenedor
docker-compose -f docker-compose.prod.yml exec django env | grep DJANGO
```
### Performance y Optimización
Si experimentas problemas de rendimiento:
1. **Verificar recursos del contenedor**:
```bash
docker stats
```
2. **Ajustar workers de Gunicorn** (editar `docker-compose.prod.yml`):
```yaml
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 8 --timeout 120
```
Regla general: `workers = (2 x CPU cores) + 1`
3. **Habilitar persistent connections**: Ya configurado en `settings/production.py` con `CONN_MAX_AGE = 600`
4. **Considerar agregar Redis para caché**: Descomentar sección de Redis en `settings/production.py`
### Seguridad
Antes de ir a producción, verificar:
- [ ] `DEBUG=False` en `.env.production`
- [ ] `SECRET_KEY` único y seguro generado
- [ ] `ALLOWED_HOSTS` configurado con dominios reales
- [ ] `CORS_ALLOWED_ORIGINS` limitado a orígenes confiables
- [ ] `CSRF_TRUSTED_ORIGINS` configurado correctamente
- [ ] Contraseñas de base de datos seguras
- [ ] `.env.production` NO commiteado a git
- [ ] HTTPS habilitado (certificados SSL/TLS configurados)
- [ ] Firewall configurado (solo puertos necesarios abiertos)
- [ ] Backups automáticos configurados
## Integraciones ## Integraciones
- **Tryton ERP**: Integración mediante sabatron-tryton-rpc-client para sincronización de clientes, productos y ventas - **Tryton ERP**: Integración mediante sabatron-tryton-rpc-client para sincronización de clientes, productos y ventas

269
README.md
View File

@@ -1,269 +0,0 @@
# Don Confiao Backend - Tienda Ilusion
Backend Django con Django REST Framework para el sistema de punto de venta Tienda Ilusion.
## Características
- 🔐 Autenticación JWT con djangorestframework-simplejwt
- 🗄️ Soporte multi-ambiente (desarrollo/producción)
- 🐘 PostgreSQL para producción, SQLite para desarrollo
- 🔄 Integración con Tryton ERP
- 📦 Docker Compose para fácil deployment
- 🛡️ Configuración de seguridad completa para producción
- 📊 API REST completa para gestión de ventas, productos y clientes
## Requisitos Previos
- Docker & Docker Compose
- Python 3.11+ (para desarrollo local sin Docker)
- Git
## Inicio Rápido
### Desarrollo Local
1. **Clonar el repositorio**
```bash
git clone <repository-url>
cd don_confiao_backend
```
2. **Iniciar servicios de desarrollo**
```bash
docker-compose -f docker-compose.dev.yml up
```
3. **Aplicar migraciones**
```bash
docker-compose -f docker-compose.dev.yml run --rm django python manage.py migrate
```
4. **Crear superusuario**
```bash
docker-compose -f docker-compose.dev.yml run --rm django python manage.py createsuperuser
```
5. **Acceder a la aplicación**
- API: http://localhost:7000
- Admin: http://localhost:7000/admin
### Producción
1. **Configurar variables de entorno**
```bash
cp .env.production.example .env.production
# Editar .env.production con valores reales
```
2. **Generar SECRET_KEY segura**
```bash
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
```
3. **Iniciar servicios**
```bash
docker-compose -f docker-compose.prod.yml up -d
```
4. **Las migraciones y collectstatic se ejecutan automáticamente**
- Si necesitas ejecutarlas manualmente:
```bash
docker-compose -f docker-compose.prod.yml exec django python manage.py migrate
docker-compose -f docker-compose.prod.yml exec django python manage.py collectstatic --noinput
```
5. **Crear superusuario**
```bash
docker-compose -f docker-compose.prod.yml exec django python manage.py createsuperuser
```
## Estructura del Proyecto
```
don_confiao_backend/
├── tienda_ilusion/ # Proyecto Django
│ ├── config/ # Configuración principal
│ │ └── settings/ # Settings por ambiente
│ │ ├── base.py # Configuración compartida
│ │ ├── development.py # Desarrollo
│ │ └── production.py # Producción
│ ├── don_confiao/ # App principal
│ └── users/ # App de usuarios
├── scripts/ # Scripts de utilidad
│ ├── health-check.sh # Verificación de salud
│ ├── backup-db.sh # Backup de base de datos
│ └── restore-backup.sh # Restore de backup
├── docker-compose.dev.yml # Docker Compose desarrollo
├── docker-compose.prod.yml # Docker Compose producción
└── requirements.txt # Dependencias Python
```
## Ambientes
### Development
- Base de datos: SQLite
- Debug: Habilitado
- CORS: Permisivo
- Server: Django development server
- Puerto: 7000
### Production
- Base de datos: PostgreSQL
- Debug: Deshabilitado
- CORS: Configurado por dominio
- Server: Gunicorn (4 workers)
- Puerto: 8000
- Seguridad: HTTPS, HSTS, secure cookies
## Comandos Útiles
### Desarrollo
```bash
# Ejecutar tests
docker-compose -f docker-compose.dev.yml run --rm django python manage.py test
# Shell de Django
docker-compose -f docker-compose.dev.yml run --rm django python manage.py shell
# Crear migraciones
docker-compose -f docker-compose.dev.yml run --rm django python manage.py makemigrations
# Ver logs
docker-compose -f docker-compose.dev.yml logs -f
```
### Producción
```bash
# Ver logs
docker-compose -f docker-compose.prod.yml logs -f django
# Backup de base de datos
./scripts/backup-db.sh
# Restore de backup
./scripts/restore-backup.sh backups/tienda_ilusion_backup_YYYYMMDD_HHMMSS.sql.gz
# Health check
./scripts/health-check.sh prod
# Reiniciar servicios
docker-compose -f docker-compose.prod.yml restart
# Detener servicios
docker-compose -f docker-compose.prod.yml down
```
## API Endpoints
La API REST está disponible en `/api/`. Principales endpoints:
- `/api/token/` - Obtener token JWT
- `/api/token/refresh/` - Refrescar token JWT
- `/api/customers/` - Gestión de clientes
- `/api/products/` - Gestión de productos
- `/api/sales/` - Gestión de ventas
- `/admin/` - Panel de administración Django
## Integración con Tryton ERP
El sistema se integra con Tryton ERP para sincronización de:
- Clientes
- Productos
- Ventas
Configurar las variables de entorno de Tryton en `.env.development` o `.env.production`:
```bash
TRYTON_HOST=your-tryton-server
TRYTON_DATABASE=your-database
TRYTON_USERNAME=your-username
TRYTON_PASSWORD=your-password
```
## Backup y Restore
### Crear Backup
```bash
./scripts/backup-db.sh
```
Los backups se guardan en `backups/` y se mantienen por 7 días.
### Restaurar Backup
```bash
./scripts/restore-backup.sh backups/backup_file.sql.gz
```
## Troubleshooting
Ver la sección de Troubleshooting en [AGENTS.md](AGENTS.md) para soluciones a problemas comunes.
### Problemas Comunes
1. **Error de conexión a base de datos**: Verificar que PostgreSQL esté corriendo
2. **CSRF errors**: Verificar `CSRF_TRUSTED_ORIGINS` en `.env.production`
3. **Static files no cargan**: Ejecutar `collectstatic`
4. **Errores de migración**: Verificar estado con `showmigrations`
## Seguridad
### Checklist de Producción
Antes de desplegar a producción:
- [ ] `DEBUG=False` en `.env.production`
- [ ] `SECRET_KEY` única y segura generada
- [ ] `ALLOWED_HOSTS` configurado correctamente
- [ ] `CORS_ALLOWED_ORIGINS` limitado a dominios confiables
- [ ] Contraseñas de base de datos seguras
- [ ] HTTPS habilitado con certificados SSL/TLS válidos
- [ ] Firewall configurado
- [ ] Backups automáticos configurados
- [ ] Monitoreo de logs configurado
## Desarrollo
### Agregar nuevas dependencias
```bash
# Agregar a requirements.txt
echo "nueva-dependencia==version" >> requirements.txt
# Reconstruir contenedor
docker-compose -f docker-compose.dev.yml build --no-cache
```
### Tests
```bash
# Ejecutar todos los tests
docker-compose -f docker-compose.dev.yml run --rm django python manage.py test
# Ejecutar tests de una app específica
docker-compose -f docker-compose.dev.yml run --rm django python manage.py test don_confiao
# Con coverage
docker-compose -f docker-compose.dev.yml run --rm django coverage run --source='.' manage.py test
docker-compose -f docker-compose.dev.yml run --rm django coverage report
```
## Contribuir
1. Fork el proyecto
2. Crear branch de feature (`git checkout -b feature/AmazingFeature`)
3. Commit cambios (`git commit -m 'Add some AmazingFeature'`)
4. Push al branch (`git push origin feature/AmazingFeature`)
5. Abrir Pull Request
## Licencia
[Especificar licencia]
## Contacto
[Información de contacto]
## Documentación Adicional
- [AGENTS.md](AGENTS.md) - Contexto completo del proyecto y troubleshooting
- [.env.production.example](.env.production.example) - Ejemplo de variables de producción

View File

@@ -4,3 +4,7 @@ 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"]

View File

@@ -9,52 +9,50 @@ post /token/
{ {
"username": "admin", "username": "admin",
"password": "admin" "password": "123"
} }
**** respuesta **** respuesta
#+begin_src json #+begin_src json
{ {
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4MDExNTc0NywiaWF0IjoxNzgwMDI5MzQ3LCJqdGkiOiIxNmVjZGMxZmY4Y2Y0MzA4ODM3ZjM5Y2ZiNjQwNmZiMCIsInVzZXJfaWQiOiIxIn0.wmN-wp3Izv0NrfL_ap_i8eyg29w-foHNrQCCL6HoZWg", "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDMxMTQ3LCJpYXQiOjE3ODAwMjkzNDcsImp0aSI6ImQ4ZjE1YTRmODc5NzRjZGViZDEzYzc1ZTU4ZDk3ZjEwIiwidXNlcl9pZCI6IjEifQ.kkQVT2pcYeS_TxlJ6QPU3rNOlZhOv96pyqVEGJI85KA" "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc"
} }
#+end_src #+end_src
*** Perfil de usuario *** Perfil de usuario
get /users/me/ get /users/me/
Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDA2MzU4LCJpYXQiOjE3ODAwMDQ1NTgsImp0aSI6IjkwNzY4OGU2MmNlNTQ1M2JiYzU5MTA2MDhmMjY1MmY5IiwidXNlcl9pZCI6IjEifQ.aFls9WyA1VuDeMMUu8t7Pa9CbbLfyvIg9pB9xIxydpU Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc
**** Respuesta **** Respuesta
#+begin_src json #+begin_src json
{ {
"id": 1, "id": 2,
"username": "admin", "username": "admin",
"email": "admin@admin.org", "email": "correo@example.com",
"first_name": "", "first_name": "",
"last_name": "", "last_name": ""
"role": "administrator"
} }
#+end_src #+end_src
*** Renovar token *** Renovar token
post /token/refresh/ post /token/refresh/
{ {
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3ODU1Njc5MywiaWF0IjoxNzc4NDcwMzkzLCJqdGkiOiJlMDU0NTVkNWExYzA0YjFkYWZhNWZkNzFkZGM5Mzc1NyIsInVzZXJfaWQiOiIxIn0.wZcbBrGoxDMPjZxI-GR1GTAuRtzU4qaT0rgGS5Oblf4" "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k"
} }
**** response **** response
#+begin_src json #+begin_src json
{ {
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc4NDcyMjQ5LCJpYXQiOjE3Nzg0NzA0NDksImp0aSI6IjE5YTM0ZDQ5Mzk3ZDQzNGE4NDlkZTgyYzdkNWQyNjQ0IiwidXNlcl9pZCI6IjEifQ.jowmaa5SXKIWpmUGLV0dj9CydYFtuecc7s_RveJvjLA" "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA"
} }
#+end_src #+end_src
** Don confiao :verb: ** Don confiao :verb:
template http://localhost:7000/don_confiao/api/ template http://localhost:7000/don_confiao/api/
Content-Type: application/json; Content-Type: application/json;
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDMxMTQ3LCJpYXQiOjE3ODAwMjkzNDcsImp0aSI6ImQ4ZjE1YTRmODc5NzRjZGViZDEzYzc1ZTU4ZDk3ZjEwIiwidXNlcl9pZCI6IjEifQ.kkQVT2pcYeS_TxlJ6QPU3rNOlZhOv96pyqVEGJI85KA Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA
*** todas las rutas *** todas las rutas
get get
**** response **** response
#+begin_src json #+begin_src json
{ {
"sales": "http://localhost:7000/don_confiao/api/sales/", "sales": "http://localhost:7000/don_confiao/api/sales/",
"catalog_sales": "http://localhost:7000/don_confiao/api/catalog_sales/",
"customers": "http://localhost:7000/don_confiao/api/customers/", "customers": "http://localhost:7000/don_confiao/api/customers/",
"products": "http://localhost:7000/don_confiao/api/products/", "products": "http://localhost:7000/don_confiao/api/products/",
"reconciliate_jar": "http://localhost:7000/don_confiao/api/reconciliate_jar/" "reconciliate_jar": "http://localhost:7000/don_confiao/api/reconciliate_jar/"
@@ -76,64 +74,3 @@ get customers/
... ...
] ]
#+end_src #+end_src
*** products
get products/
*** Productos Inactivos
get products/?active=false
*** Productos Activos
get products/?active=true
*** Traer todos los productos
get products/?active=all
*** Inactiva productos
patch products/1
{
"active": false
}
*** Obtener Ventas por catalogo
get catalog_sales/
**** response
#+begin_src json
{
"id": 6,
"customer": 1,
"date": "2024-09-02T00:00:00Z",
"catalogsaleline_set": [
{
"id": 2,
"catalog_sale": 6,
"product": 1,
"unit_price": "3000.00",
"quantity": "2.00"
},
{
"id": 3,
"catalog_sale": 6,
"product": 1,
"unit_price": "5000.00",
"quantity": "3.00"
}
],
"total": 21000.0
}
*** Crear una venta por catalogo
post catalog_sales/
{"customer": "1", "date": "2024-09-02", "catalogsaleline_set": [{"product": "1", "quantity": "2", "unit_price": "3000"}, {"product": "1", "quantity": "3", "unit_price": "5000"}]}
*** Importar Clientes de Tryton
post importar_productos_de_tryton
{}
**** response
#+begin_src json
[]
#+end_src

View File

@@ -1,21 +0,0 @@
services:
django:
build:
context: ./
dockerfile: django.Dockerfile
container_name: tienda_ilusion_django_dev
env_file:
- .env.development
environment:
- DJANGO_ENV=development
volumes:
- ./tienda_ilusion:/app/
ports:
- "7000:9090"
networks:
- tienda_network_dev
command: python manage.py runserver 0.0.0.0:9090
networks:
tienda_network_dev:
driver: bridge

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,89 +0,0 @@
services:
postgres:
image: postgres:15-alpine
container_name: tienda_ilusion_postgres_prod
environment:
- POSTGRES_USER=${DB_USER:-tienda_ilusion_user}
- POSTGRES_DB=${DB_NAME:-tienda_ilusion_prod}
- POSTGRES_PASSWORD=${DB_PASSWORD:-tienda_ilusion_pass}
env_file:
- .env.production
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- tienda_network
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-tienda_ilusion_user} -d ${POSTGRES_DB:-tienda_ilusion_prod}",
]
interval: 10s
timeout: 5s
retries: 5
django:
build:
context: ./
dockerfile: django.Dockerfile
container_name: tienda_ilusion_django_prod
env_file:
- .env.production
environment:
- DJANGO_ENV=production
- DB_HOST=postgres
- DB_USER=${DB_USER:-tienda_ilusion_user}
- DB_NAME=${DB_NAME:-tienda_ilusion_prod}
- DB_PASSWORD=${DB_PASSWORD:-tienda_ilusion_pass}
volumes:
- ./tienda_ilusion:/app/
- static_volume:/app/staticfiles
- media_volume:/app/media
- logs_volume:/app/logs
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
networks:
- tienda_network
restart: unless-stopped
command: >
sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput &&
gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4 --timeout 120"
# Optional: Nginx reverse proxy para servir archivos estáticos
# nginx:
# image: nginx:alpine
# container_name: tienda_ilusion_nginx
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# - static_volume:/var/www/static:ro
# - media_volume:/var/www/media:ro
# - ./ssl:/etc/nginx/ssl:ro
# depends_on:
# - django
# networks:
# - tienda_network
# restart: unless-stopped
volumes:
postgres_data:
driver: local
static_volume:
driver: local
media_volume:
driver: local
logs_volume:
driver: local
networks:
tienda_network:
driver: bridge

View File

@@ -1,71 +0,0 @@
services:
postgres:
image: postgres:15-alpine
container_name: tienda_ilusion_postgres_staging
environment:
- POSTGRES_USER=${DB_USER:-tienda_ilusion_user}
- POSTGRES_DB=${DB_NAME:-tienda_ilusion_staging}
- POSTGRES_PASSWORD=${DB_PASSWORD:-staging_local_password}
env_file:
- .env.staging
volumes:
- postgres_staging_data:/var/lib/postgresql/data
ports:
- "5433:5432" # Puerto diferente para no conflictuar con prod
networks:
- tienda_staging_network
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USER:-tienda_ilusion_user} -d ${DB_PASSWORD:-tienda_ilusion_staging}",
]
interval: 10s
timeout: 5s
retries: 5
django:
build:
context: ./
dockerfile: django.Dockerfile
container_name: tienda_ilusion_django_staging
env_file:
- .env.staging
environment:
- DJANGO_ENV=staging
- DB_HOST=postgres
- DB_USER=${DB_USER:-tienda_ilusion_user}
- DB_NAME=${DB_NAME:-tienda_ilusion_staging}
- DB_PASSWORD=${DB_PASSWORD:-staging_local_password}
volumes:
- ./tienda_ilusion:/app/
- static_staging_volume:/app/staticfiles
- media_staging_volume:/app/media
- logs_staging_volume:/app/logs
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
networks:
- tienda_staging_network
restart: unless-stopped
command: >
sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput &&
gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4 --timeout 120 --reload"
volumes:
postgres_staging_data:
driver: local
static_staging_volume:
driver: local
media_staging_volume:
driver: local
logs_staging_volume:
driver: local
networks:
tienda_staging_network:
driver: bridge

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
django:
image: gitea.onecluster.org/oneteam/don_confiao_backend:latest
env_file:
- .env
ports:
- "7000:9090"

288
poetry.lock generated
View File

@@ -1,288 +0,0 @@
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.11.1"
description = "ASGI specs, helper code, and adapters"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"},
{file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"},
]
[package.extras]
tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "django"
version = "6.0.5"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
{file = "django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0"},
{file = "django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269"},
]
[package.dependencies]
asgiref = ">=3.9.1"
sqlparse = ">=0.5.0"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
argon2 = ["argon2-cffi (>=23.1.0)"]
bcrypt = ["bcrypt (>=4.1.1)"]
[[package]]
name = "django-cors-headers"
version = "4.9.0"
description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449"},
{file = "django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8"},
]
[package.dependencies]
asgiref = ">=3.6"
django = ">=4.2"
[[package]]
name = "djangorestframework"
version = "3.17.1"
description = "Web APIs for Django, made easy."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457"},
{file = "djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5"},
]
[package.dependencies]
django = ">=4.2"
[[package]]
name = "djangorestframework-simplejwt"
version = "5.5.1"
description = "A minimal JSON Web Token authentication plugin for Django REST Framework"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469"},
{file = "djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f"},
]
[package.dependencies]
django = ">=4.2"
djangorestframework = ">=3.14"
pyjwt = ">=1.7.1"
[package.extras]
crypto = ["cryptography (>=3.3.1)"]
dev = ["Sphinx", "cryptography", "freezegun", "ipython", "pre-commit", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "pyupgrade", "ruff", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel", "yesqa"]
doc = ["Sphinx", "sphinx_rtd_theme (>=0.1.9)"]
lint = ["pre-commit", "pyupgrade", "ruff", "yesqa"]
python-jose = ["python-jose (==3.3.0)"]
test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"]
[[package]]
name = "mslex"
version = "1.3.0"
description = "shlex for windows"
optional = false
python-versions = ">=3.5"
groups = ["dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4"},
{file = "mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d"},
]
[[package]]
name = "psutil"
version = "6.1.1"
description = "Cross-platform lib for process and system monitoring in Python."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["dev"]
files = [
{file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"},
{file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"},
{file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"},
{file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"},
{file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"},
{file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"},
{file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"},
{file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"},
{file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"},
{file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"},
{file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"},
{file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"},
{file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"},
{file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"},
]
[package.extras]
dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"]
test = ["pytest", "pytest-xdist", "setuptools"]
[[package]]
name = "pyjwt"
version = "2.12.1"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"},
{file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"},
]
[package.extras]
crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"]
[[package]]
name = "sabatron-tryton-rpc-client"
version = "7.4.0"
description = "Python RPC Client for Tryton"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "sabatron_tryton_rpc_client-7.4.0-py3-none-any.whl", hash = "sha256:131d8d9d6a1dc1d556d4806fa7750ca637247f021881dca5c2855d8a44e4bd45"},
{file = "sabatron_tryton_rpc_client-7.4.0.tar.gz", hash = "sha256:3d3ededd1a8488463d6338cd7d8b2a271834ba42fc220ebb8e15e983c0e5b19d"},
]
[[package]]
name = "sqlparse"
version = "0.5.5"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
{file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
]
[package.extras]
dev = ["build"]
doc = ["sphinx"]
[[package]]
name = "taskipy"
version = "1.14.1"
description = "tasks runner for python projects"
optional = false
python-versions = "<4.0,>=3.6"
groups = ["dev"]
files = [
{file = "taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1"},
{file = "taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed"},
]
[package.dependencies]
colorama = ">=0.4.4,<0.5.0"
mslex = {version = ">=1.1.0,<2.0.0", markers = "sys_platform == \"win32\""}
psutil = ">=5.7.2,<7"
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
[[package]]
name = "tomli"
version = "2.4.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"},
{file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"},
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"},
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"},
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"},
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"},
{file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"},
{file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"},
{file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"},
{file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"},
{file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"},
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"},
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"},
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"},
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"},
{file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"},
{file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"},
{file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"},
{file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"},
{file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"},
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"},
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"},
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"},
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"},
{file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"},
{file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"},
{file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"},
{file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"},
{file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"},
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"},
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"},
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"},
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"},
{file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"},
{file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"},
{file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"},
{file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"},
{file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"},
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"},
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"},
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"},
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"},
{file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"},
{file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"},
{file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"},
{file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"},
{file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"},
]
[[package]]
name = "tzdata"
version = "2026.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main"]
markers = "sys_platform == \"win32\""
files = [
{file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"},
{file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"},
]
[metadata]
lock-version = "2.1"
python-versions = ">=3.14,<4.0"
content-hash = "02efc37f4afc4dbe36959374dba58e3b6e61be3f8c4298427009b9ee0d3a1910"

View File

@@ -1,50 +0,0 @@
[project]
name = "don-confiao-backend"
version = "0.1.0"
description = "Shop for Recreo store"
authors = [
{name = "aserrador",email = "alejandro.ayala@onecluster.org"}
]
license = {text = "GPL-3.0-or-later"}
requires-python = ">=3.14,<4.0"
dependencies = [
"django (>=6.0.5,<7.0.0)",
"djangorestframework (>=3.17.1,<4.0.0)",
"django-cors-headers (>=4.9.0,<5.0.0)",
"djangorestframework-simplejwt (>=5.5.1,<6.0.0)",
"sabatron-tryton-rpc-client (>=7.4.0,<8.0.0)"
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[dependency-groups]
dev = [
"taskipy (>=1.14.1,<2.0.0)"
]
[tool.taskipy.tasks]
dev-up = "docker compose -f docker-compose.dev.yml up -d"
dev-logs = "docker compose -f docker-compose.dev.yml logs -f -n 50"
dev-stop = "docker compose -f docker-compose.dev.yml stop"
dev-restart = "docker compose -f docker-compose.dev.yml restart"
dev-down = "docker compose -f docker-compose.dev.yml down -vv --rmi all"
dev-tail = "docker compose -f docker-compose.dev.yml logs -f"
dev-sh = "docker compose -f docker-compose.dev.yml exec -it --user root django bash"
dev-migrate = "docker compose -f docker-compose.dev.yml exec -it --user root django python3 manage.py migrate"
dev-createsuperuser = "docker compose -f docker-compose.dev.yml exec -it --user root django python3 manage.py createsuperuser"
dev-test = "docker compose -f docker-compose.dev.yml exec -it --user root django python3 manage.py test"
live-up = "docker compose -f docker-compose.staging.yml up -d"
live-logs = "docker compose -f docker-compose.staging.yml logs -f -n 50"
live-down = "docker compose -f docker-compose.staging.yml down -vv --rmi all"
live-sh = "docker compose -f docker-compose.staging.yml exec -it --user root django bash"
prod-up = "docker compose -f docker-compose.prod.yml up -d"
prod-logs = "docker compose -f docker-compose.prod.yml logs -f -n 50"
prod-down = "docker compose -f docker-compose.prod.yml down -vv --rmi all"
prod-sh = "docker compose -f docker-compose.prod.yml exec -it --user root django bash"

View File

@@ -3,15 +3,3 @@ djangorestframework
django-cors-headers django-cors-headers
djangorestframework-simplejwt djangorestframework-simplejwt
sabatron-tryton-rpc-client==7.4.0 sabatron-tryton-rpc-client==7.4.0
# Database drivers
psycopg2-binary # PostgreSQL driver for production
# Production server
gunicorn # WSGI HTTP Server for production
# Environment variable management
python-decouple # Manage environment variables and settings
# Static files serving in production/staging
whitenoise==6.6.0 # Serve static files efficiently with compression

View File

@@ -1,43 +0,0 @@
"""
Settings module for tienda_ilusion project.
This module automatically loads the appropriate settings based on the DJANGO_ENV
environment variable. Valid values are:
- 'development' (default): Development settings with SQLite
- 'staging': Staging settings for testing production config locally (PostgreSQL, no SSL)
- 'production': Production settings with full security (PostgreSQL, SSL, etc.)
Usage:
Set DJANGO_ENV environment variable before running Django:
Development:
export DJANGO_ENV=development
python manage.py runserver
Staging (test production locally):
export DJANGO_ENV=staging
python manage.py runserver
Production:
export DJANGO_ENV=production
gunicorn config.wsgi:application
"""
import os
# Determine which environment settings to load
DJANGO_ENV = os.environ.get("DJANGO_ENV", "development")
if DJANGO_ENV == "production":
from .production import *
elif DJANGO_ENV == "staging":
from .staging import *
elif DJANGO_ENV == "development":
from .development import *
else:
# Fallback to development if unknown environment
from .development import *
print(
f"Warning: Unknown DJANGO_ENV '{DJANGO_ENV}', falling back to development settings"
)

View File

@@ -1,176 +0,0 @@
"""
Django settings for tienda_ilusion project.
Generated by 'django-admin startproject' using Django 5.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
# BASE_DIR apunta a tienda_ilusion/ (3 niveles arriba desde settings/base.py)
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# This will be overridden in production settings
SECRET_KEY = os.environ.get(
"SECRET_KEY",
"django-insecure-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v",
)
# SECURITY WARNING: don't run with debug turned on in production!
# This will be overridden in each environment
DEBUG = True
# This will be overridden in each environment
ALLOWED_HOSTS = []
# This will be overridden in each environment
CORS_ALLOWED_ORIGINS = []
# Application definition
INSTALLED_APPS = [
"don_confiao.apps.DonConfiaoConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework.authtoken",
"corsheaders",
"users",
# 'don_confiao'
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # Serve static files (must be after SecurityMiddleware)
"corsheaders.middleware.CorsMiddleware", # Must be before CommonMiddleware
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "tienda_ilusion/templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
# This will be overridden in each environment
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
FIXTURE_DIRS = ["don_confiao/tests/Fixtures"]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"AUTH_HEADER_TYPES": ("Bearer",),
}
# Logging configuration (can be overridden in environment settings)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}

View File

@@ -1,115 +0,0 @@
"""
Development settings for tienda_ilusion project.
This file contains settings specific to local development environment.
Uses SQLite database and permissive CORS settings for easier development.
"""
import os
from .base import *
# SECURITY WARNING: don't use this in production!
DEBUG = True
# SECRET_KEY for development (insecure, but convenient for development)
SECRET_KEY = os.environ.get(
"SECRET_KEY",
"django-insecure-development-key-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v",
)
# Allow all hosts in development for easier testing
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0").split(
","
)
# CORS settings for development
CORS_ALLOWED_ORIGINS = os.environ.get(
"CORS_ALLOWED_ORIGINS",
"http://localhost:3000,http://localhost:7001,http://localhost:5173",
).split(",")
# Allow credentials in CORS for development
CORS_ALLOW_CREDENTIALS = True
# Database - SQLite for development
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Email backend for development (prints to console)
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Static files configuration for development
# Note: With DEBUG=True, Django's runserver serves static files automatically
# But WhiteNoise can still be used for consistency across environments
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
# WhiteNoise configuration for development (optional, for consistency)
# In development with DEBUG=True, Django serves static files automatically
# But this ensures consistent behavior across all environments
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage", # No manifest in dev
},
}
# WhiteNoise settings for development
WHITENOISE_AUTOREFRESH = True # Auto-reload static files
WHITENOISE_USE_FINDERS = True # Use Django's staticfiles finders (convenient for dev)
WHITENOISE_MANIFEST_STRICT = False # Permissive mode
# Enhanced logging for development
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "[{levelname}] {asctime} {module} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
},
"loggers": {
"django": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"django.db.backends": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
},
},
}
# Development-only apps (optional)
# Uncomment to add django-debug-toolbar or other dev tools
# INSTALLED_APPS += [
# 'debug_toolbar',
# ]
# MIDDLEWARE += [
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
# ]
# INTERNAL_IPS for debug toolbar
# INTERNAL_IPS = [
# '127.0.0.1',
# ]

View File

@@ -1,192 +0,0 @@
"""
Production settings for tienda_ilusion project.
This file contains settings specific to production environment.
Uses PostgreSQL database and strict security settings.
IMPORTANT: All sensitive values MUST be provided via environment variables.
"""
import os
from .base import *
# SECURITY WARNING: DEBUG must be False in production!
DEBUG = False
# SECRET_KEY must be provided via environment variable in production
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable must be set in production!")
# ALLOWED_HOSTS must be provided via environment variable
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
if not ALLOWED_HOSTS or ALLOWED_HOSTS == [""]:
raise ValueError("ALLOWED_HOSTS environment variable must be set in production!")
# CORS settings for production
CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ALLOWED_ORIGINS", "").split(",")
if not CORS_ALLOWED_ORIGINS or CORS_ALLOWED_ORIGINS == [""]:
raise ValueError(
"CORS_ALLOWED_ORIGINS environment variable must be set in production!"
)
CORS_ALLOW_CREDENTIALS = True
# Database - PostgreSQL for production
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "tienda_ilusion"),
"USER": os.environ.get("DB_USER", "postgres"),
"PASSWORD": os.environ.get("DB_PASSWORD"),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
"CONN_MAX_AGE": 600, # Persistent connections
}
}
if not DATABASES["default"]["PASSWORD"]:
raise ValueError("DB_PASSWORD environment variable must be set in production!")
# Security settings for HTTPS
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
# HTTP Strict Transport Security (HSTS)
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Proxy headers for deployment behind reverse proxy (nginx, etc.)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Email configuration for production
# Adjust based on your email service
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "True") == "True"
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@tiendailusion.com")
# Static files configuration for production
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
# Media files configuration
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
# WhiteNoise configuration for serving static files in production
# Django 4.2+ uses STORAGES setting instead of STATICFILES_STORAGE
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# WhiteNoise settings optimized for production
WHITENOISE_AUTOREFRESH = False # Disabled in production for better performance
WHITENOISE_USE_FINDERS = False # Use collected static files only
WHITENOISE_MANIFEST_STRICT = True # Strict mode - fail if referenced file is missing
WHITENOISE_MAX_AGE = 31536000 # 1 year cache for files with content hash
WHITENOISE_ALLOW_ALL_ORIGINS = False # Security: don't allow CORS for static files
# Production logging - log to file and console
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "[{levelname}] {asctime} {name} {message}",
"style": "{",
},
"simple": {
"format": "[{levelname}] {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "django.log",
"maxBytes": 1024 * 1024 * 15, # 15MB
"backupCount": 10,
"formatter": "verbose",
},
},
"root": {
"handlers": ["console", "file"],
"level": "INFO",
},
"loggers": {
"django": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"django.security": {
"handlers": ["console", "file"],
"level": "WARNING",
"propagate": False,
},
},
}
# Create logs directory if it doesn't exist
import pathlib
logs_dir = BASE_DIR / "logs"
pathlib.Path(logs_dir).mkdir(parents=True, exist_ok=True)
# Admin email notifications for errors (optional)
ADMINS = [
("Admin", os.environ.get("ADMIN_EMAIL", "admin@tiendailusion.com")),
]
MANAGERS = ADMINS
# Cache configuration (optional - adjust based on your setup)
# Using local memory cache by default
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}
# For Redis cache (recommended for production):
# CACHES = {
# "default": {
# "BACKEND": "django_redis.cache.RedisCache",
# "LOCATION": os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/1"),
# "OPTIONS": {
# "CLIENT_CLASS": "django_redis.client.DefaultClient",
# }
# }
# }
# Session configuration
SESSION_ENGINE = "django.contrib.sessions.backends.db"
SESSION_COOKIE_AGE = 86400 # 24 hours
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
# CSRF configuration
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Lax"
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",")
# Performance optimizations
CONN_MAX_AGE = 600 # Database connection pooling

View File

@@ -1,206 +0,0 @@
"""
Staging settings for tienda_ilusion project.
This file contains settings for staging environment - simulates production
configuration but runs on localhost without SSL redirect for easier testing.
Use this environment to test production-like configuration locally before
deploying to real production.
"""
import os
from .base import *
# SECURITY WARNING: DEBUG is False to simulate production behavior
DEBUG = False
# SECRET_KEY for staging (use a fixed key for local staging, not for real production)
SECRET_KEY = os.environ.get(
"SECRET_KEY",
"staging-local-key-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v",
)
# Allow localhost for staging
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0").split(
","
)
# CORS settings for staging - permissive for localhost testing
CORS_ALLOWED_ORIGINS = os.environ.get(
"CORS_ALLOWED_ORIGINS",
"http://localhost:8000,http://localhost:3000,http://localhost:5173,http://localhost:7001,http://127.0.0.1:8000",
).split(",")
CORS_ALLOW_CREDENTIALS = True
# Additional CORS headers for better compatibility
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
CORS_EXPOSE_HEADERS = ["Content-Type", "X-CSRFToken"]
# Database - PostgreSQL for staging (simulates production)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "tienda_ilusion_staging"),
"USER": os.environ.get("DB_USER", "tienda_ilusion_user"),
"PASSWORD": os.environ.get("DB_PASSWORD", "staging_local_password"),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
"CONN_MAX_AGE": 600, # Persistent connections
}
}
# Security settings - DISABLED SSL redirect for localhost testing
# This is the key difference from production.py
SECURE_SSL_REDIRECT = False # Permite HTTP en localhost
SESSION_COOKIE_SECURE = False # Permite cookies sin HTTPS
CSRF_COOKIE_SECURE = False # Permite CSRF sin HTTPS
# Keep other security features enabled
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
# NO HSTS for staging (only for production with real HTTPS)
SECURE_HSTS_SECONDS = 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
SECURE_HSTS_PRELOAD = False
# Proxy headers - disabled for staging
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Email configuration for staging - use console backend for easier debugging
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Optional: Use SMTP for staging if you want to test real emails
# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
# EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
# EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "True") == "True"
# EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
# EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
# DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "staging@tiendailusion.local")
# Static files configuration
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
# Media files configuration
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
# WhiteNoise configuration for serving static files in staging
# Django 4.2+ uses STORAGES setting instead of STATICFILES_STORAGE
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# WhiteNoise settings for staging
WHITENOISE_AUTOREFRESH = (
True # Auto-reload static files in staging (convenient for testing)
)
WHITENOISE_USE_FINDERS = False # Use collected static files only
WHITENOISE_MANIFEST_STRICT = False # More permissive in staging
WHITENOISE_MAX_AGE = 600 # 10 minutes cache for staging (allows easier testing)
# Staging logging - similar to production but more verbose
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "[{levelname}] {asctime} {name} {message}",
"style": "{",
},
"simple": {
"format": "[{levelname}] {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "staging.log",
"maxBytes": 1024 * 1024 * 15, # 15MB
"backupCount": 10,
"formatter": "verbose",
},
},
"root": {
"handlers": ["console", "file"],
"level": "INFO",
},
"loggers": {
"django": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"django.security": {
"handlers": ["console", "file"],
"level": "WARNING",
"propagate": False,
},
"django.db.backends": {
"handlers": ["console"],
"level": "INFO", # Show SQL queries in staging for debugging
"propagate": False,
},
},
}
# Create logs directory if it doesn't exist
import pathlib
logs_dir = BASE_DIR / "logs"
pathlib.Path(logs_dir).mkdir(parents=True, exist_ok=True)
# Admin email notifications
ADMINS = [
("Staging Admin", os.environ.get("ADMIN_EMAIL", "admin@tiendailusion.local")),
]
MANAGERS = ADMINS
# Cache configuration - local memory cache for staging
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "staging-cache",
}
}
# Session configuration
SESSION_ENGINE = "django.contrib.sessions.backends.db"
SESSION_COOKIE_AGE = 86400 # 24 hours
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
# CSRF configuration
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Lax"
CSRF_TRUSTED_ORIGINS = os.environ.get(
"CSRF_TRUSTED_ORIGINS", "http://localhost:8000,http://127.0.0.1:8000"
).split(",")
# Performance optimizations
CONN_MAX_AGE = 600 # Database connection pooling

View File

@@ -1,21 +1,11 @@
from django.contrib import admin from django.contrib import admin
from .models.sales import Sale, SaleLine from .models import (
from .models.customers import Customer Customer, Sale, SaleLine, Product, ProductCategory, Payment,
from .models.sales import ( ReconciliationJar)
Sale,
SaleLine,
CatalogSale,
CatalogSaleLine,
Payment,
)
from .models.products import Product, ProductCategory
from .models.payments import ReconciliationJar
admin.site.register(Customer) admin.site.register(Customer)
admin.site.register(Sale) admin.site.register(Sale)
admin.site.register(SaleLine) admin.site.register(SaleLine)
admin.site.register(CatalogSale)
admin.site.register(CatalogSaleLine)
admin.site.register(Product) admin.site.register(Product)
admin.site.register(ProductCategory) admin.site.register(ProductCategory)
admin.site.register(Payment) admin.site.register(Payment)

View File

@@ -5,29 +5,8 @@ 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 rest_framework.permissions import IsAuthenticated
from .models.sales import Sale, SaleLine from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
from .models.customers import Customer from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
from .models.sales import (
Sale,
SaleLine,
CatalogSale,
CatalogSaleLine,
Payment,
)
from .models.products import Product, ProductCategory
from .models.payments import PaymentMethods, ReconciliationJar
from .models.admin import AdminCode
from .serializers import (
SaleSerializer,
CatalogSaleSerializer,
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 .permissions import IsAdministrator
@@ -37,10 +16,10 @@ import io
import csv import csv
import os import os
TRYTON_HOST = os.environ.get("TRYTON_HOST", "localhost") TRYTON_HOST = os.environ.get('TRYTON_HOST', 'localhost')
TRYTON_DATABASE = os.environ.get("TRYTON_DATABASE", "tryton") TRYTON_DATABASE = os.environ.get('TRYTON_DATABASE', 'tryton')
TRYTON_USERNAME = os.environ.get("TRYTON_USERNAME", "admin") TRYTON_USERNAME = os.environ.get('TRYTON_USERNAME', 'admin')
TRYTON_PASSWORD = os.environ.get("TRYTON_PASSWORD", "admin") TRYTON_PASSWORD = os.environ.get('TRYTON_PASSWORD', 'admin')
TRYTON_COP_CURRENCY = 31 TRYTON_COP_CURRENCY = 31
TRYTON_COMPANY_ID = 1 TRYTON_COMPANY_ID = 1
TRYTON_SHOPS = [1] TRYTON_SHOPS = [1]
@@ -48,7 +27,7 @@ TRYTON_SHOPS = [1]
class Pagination(PageNumberPagination): class Pagination(PageNumberPagination):
page_size = 10 page_size = 10
page_size_query_param = "page_size" page_size_query_param = 'page_size'
class SaleView(viewsets.ModelViewSet): class SaleView(viewsets.ModelViewSet):
@@ -57,72 +36,39 @@ class SaleView(viewsets.ModelViewSet):
def create(self, request): def create(self, request):
data = request.data data = request.data
customer = Customer.objects.get(pk=data["customer"]) customer = Customer.objects.get(pk=data['customer'])
date = data["date"] date = data['date']
lines = data["saleline_set"] lines = data['saleline_set']
payment_method = data["payment_method"] payment_method = data['payment_method']
description = data.get("notes", "") description = data.get('notes', '')
sale = Sale.objects.create( sale = Sale.objects.create(
customer=customer, customer=customer,
date=date, date=date,
payment_method=payment_method, payment_method=payment_method,
description=description, description=description
) )
for line in lines: for line in lines:
product = Product.objects.get(pk=line["product"]) product = Product.objects.get(pk=line['product'])
quantity = line["quantity"] quantity = line['quantity']
unit_price = line["unit_price"] unit_price = line['unit_price']
SaleLine.objects.create( SaleLine.objects.create(
sale=sale, sale=sale,
product=product, product=product,
quantity=quantity, quantity=quantity,
unit_price=unit_price, unit_price=unit_price
) )
return Response( return Response(
{"id": sale.id, "message": "Venta creada con exito"}, {'id': sale.id, 'message': 'Venta creada con exito'},
status=201, status=201
) )
class CatalogSaleView(viewsets.ModelViewSet):
queryset = CatalogSale.objects.all()
serializer_class = CatalogSaleSerializer
class ProductView(viewsets.ModelViewSet): class ProductView(viewsets.ModelViewSet):
queryset = Product.objects.all() queryset = Product.objects.all()
serializer_class = ProductSerializer serializer_class = ProductSerializer
def get_queryset(self):
"""
Filters products by active status for list operations.
Detail operations (retrieve, update, destroy) return all products.
Query params for list:
- active=true (default): Only active products
- active=false: Only inactive products
- active=all: All products regardless of status
"""
queryset = Product.objects.all()
# Only filter for list action, not for detail operations
if self.action != "list":
return queryset
active_param = self.request.query_params.get("active", "true")
if active_param.lower() == "all":
return queryset
elif active_param.lower() in ["true", "1", "yes"]:
return queryset.filter(active=True)
elif active_param.lower() in ["false", "0", "no"]:
return queryset.filter(active=False)
else:
# Default behavior: return only active products
return queryset.filter(active=True)
class CustomerView(viewsets.ModelViewSet): class CustomerView(viewsets.ModelViewSet):
queryset = Customer.objects.all() queryset = Customer.objects.all()
@@ -134,24 +80,20 @@ class ReconciliateJarView(APIView):
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')
serializer = ReconciliationJarSerializer(data=data) serializer = ReconciliationJarSerializer(data=data)
if serializer.is_valid(): if serializer.is_valid():
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id) cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
if not self._is_valid_total( if not self._is_valid_total(cash_purchases, data.get('total_cash_purchases')):
cash_purchases, data.get("total_cash_purchases")
):
return Response( return Response(
{ {'error': 'total_cash_purchases not equal to sum of all purchases.'},
"error": "total_cash_purchases not equal to sum of all purchases." status=HTTP_400_BAD_REQUEST
},
status=HTTP_400_BAD_REQUEST,
) )
reconciliation = serializer.save() reconciliation = serializer.save()
other_purchases = self._get_other_purchases(data.get("other_totals")) other_purchases = self._get_other_purchases(data.get('other_totals'))
self._link_purchases(reconciliation, cash_purchases, other_purchases) self._link_purchases(reconciliation, cash_purchases, other_purchases)
return Response({"id": reconciliation.id}) return Response({'id': reconciliation.id})
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
def get(self, request): def get(self, request):
@@ -161,16 +103,15 @@ class ReconciliateJarView(APIView):
def _is_valid_total(self, purchases, total): def _is_valid_total(self, purchases, total):
calculated_total = sum(p.get_total() for p in purchases) calculated_total = sum(p.get_total() for p in purchases)
return Decimal(calculated_total).quantize(Decimal(".0001")) == ( return Decimal(calculated_total).quantize(Decimal('.0001')) == (
Decimal(total).quantize(Decimal(".0001")) Decimal(total).quantize(Decimal('.0001')))
)
def _get_other_purchases(self, other_totals): def _get_other_purchases(self, other_totals):
if not other_totals: if not other_totals:
return [] return []
purchases = [] purchases = []
for method in other_totals: for method in other_totals:
purchases.extend(other_totals[method]["purchases"]) purchases.extend(other_totals[method]['purchases'])
if purchases: if purchases:
return Sale.objects.filter(pk__in=purchases) return Sale.objects.filter(pk__in=purchases)
return [] return []
@@ -221,11 +162,11 @@ class AdminCodeValidateView(APIView):
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)})
class ReconciliateJarModelView(viewsets.ModelViewSet): 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] permission_classes = [IsAuthenticated, IsAdministrator]
@@ -237,7 +178,7 @@ class SalesForTrytonView(APIView):
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)
return Response({"csv": csv}) return Response({'csv': csv})
def _generate_sales_CSV(self, sales): def _generate_sales_CSV(self, sales):
output = io.StringIO() output = io.StringIO()
@@ -257,14 +198,12 @@ class SalesToTrytonView(APIView):
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
database=TRYTON_DATABASE, database=TRYTON_DATABASE,
username=TRYTON_USERNAME, username=TRYTON_USERNAME,
password=TRYTON_PASSWORD, password=TRYTON_PASSWORD
) )
tryton_client.connect() tryton_client.connect()
method = "model.sale.sale.create" method = 'model.sale.sale.create'
tryton_context = { tryton_context = {'company': TRYTON_COMPANY_ID,
"company": TRYTON_COMPANY_ID, 'shops': TRYTON_SHOPS}
"shops": TRYTON_SHOPS,
}
successful = [] successful = []
failed = [] failed = []
@@ -279,11 +218,15 @@ class SalesToTrytonView(APIView):
sale.save() sale.save()
successful.append(sale.id) successful.append(sale.id)
except Exception as e: except Exception as e:
print(f"Error al enviar la venta: {e}venta_id: {sale.id}") print(f"Error al enviar la venta: {e}"
f"venta_id: {sale.id}")
failed.append(sale.id) failed.append(sale.id)
continue continue
return Response({"successful": successful, "failed": failed}, status=200) return Response(
{'successful': successful, 'failed': failed},
status=200
)
def __to_tryton_params(self, sale, lines, tryton_context): def __to_tryton_params(self, sale, lines, tryton_context):
sale_tryton = TrytonSale(sale, lines) sale_tryton = TrytonSale(sale, lines)
@@ -291,17 +234,14 @@ class SalesToTrytonView(APIView):
class TrytonSale: class TrytonSale:
def __init__(self, sale, lines): def __init__(self, sale, lines):
self.sale = sale self.sale = sale
self.lines = lines self.lines = lines
def _format_date(self, _date): def _format_date(self, _date):
return { return {"__class__": "date", "year": _date.year, "month": _date.month,
"__class__": "date", "day": _date.day}
"year": _date.year,
"month": _date.month,
"day": _date.day,
}
def to_tryton(self): def to_tryton(self):
return { return {
@@ -309,17 +249,17 @@ class TrytonSale:
"shipment_address": self.sale.customer.address_external_id, "shipment_address": self.sale.customer.address_external_id,
"invoice_address": self.sale.customer.address_external_id, "invoice_address": self.sale.customer.address_external_id,
"currency": TRYTON_COP_CURRENCY, "currency": TRYTON_COP_CURRENCY,
"comment": self.sale.description or "", "comment": self.sale.description or '',
"description": "Metodo pago: " + str(self.sale.payment_method or ""), "description": "Metodo pago: " + str(
self.sale.payment_method or ''
),
"party": self.sale.customer.external_id, "party": self.sale.customer.external_id,
"reference": "don_confiao " + str(self.sale.id), "reference": "don_confiao " + str(self.sale.id),
"sale_date": self._format_date(self.sale.date), "sale_date": self._format_date(self.sale.date),
"lines": [ "lines": [[
[ "create",
"create", [TrytonLineSale(line).to_tryton() for line in self.lines]
[TrytonLineSale(line).to_tryton() for line in self.lines], ]],
]
],
"self_pick_up": True, "self_pick_up": True,
} }
@@ -337,7 +277,7 @@ class TrytonLineSale:
"quantity": self._format_decimal(self.sale_line.quantity), "quantity": self._format_decimal(self.sale_line.quantity),
"type": "line", "type": "line",
"unit": self.sale_line.product.unit_external_id, "unit": self.sale_line.product.unit_external_id,
"unit_price": self._format_decimal(self.sale_line.unit_price), "unit_price": self._format_decimal(self.sale_line.unit_price)
} }
@@ -349,18 +289,12 @@ class ProductsFromTrytonView(APIView):
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
database=TRYTON_DATABASE, database=TRYTON_DATABASE,
username=TRYTON_USERNAME, username=TRYTON_USERNAME,
password=TRYTON_PASSWORD, password=TRYTON_PASSWORD
) )
tryton_client.connect() tryton_client.connect()
method = "model.product.product.search" method = 'model.product.product.search'
context = {"company": 1} context = {'company': 1}
params = [ params = [[["salable", "=", True]], 0, 1000, [["rec_name", "ASC"], ["id", None]], context]
[["salable", "=", True]],
0,
1000,
[["rec_name", "ASC"], ["id", None]],
context,
]
product_ids = tryton_client.call(method, params) product_ids = tryton_client.call(method, params)
tryton_products = self.__get_product_datails_from_tryton( tryton_products = self.__get_product_datails_from_tryton(
product_ids, tryton_client, context product_ids, tryton_client, context
@@ -373,17 +307,18 @@ class ProductsFromTrytonView(APIView):
for tryton_product in tryton_products: for tryton_product in tryton_products:
try: try:
product = Product.objects.get(external_id=tryton_product.get("id")) product = Product.objects.get(
external_id=tryton_product.get('id')
)
except Product.DoesNotExist: except Product.DoesNotExist:
try: try:
product = self.__create_product(tryton_product) product = self.__create_product(tryton_product)
created_products.append(product.id) created_products.append(product.id)
continue continue
except Exception as e: except Exception as e:
print( print(f"Error al importar productos: {e}"
f"Error al importar productos: {e}El producto: {tryton_product}" f"El producto: {tryton_product}")
) failed_products.append(tryton_product.get('id'))
failed_products.append(tryton_product.get("id"))
continue continue
if self.__need_update(product, tryton_product): if self.__need_update(product, tryton_product):
@@ -394,55 +329,50 @@ class ProductsFromTrytonView(APIView):
return Response( return Response(
{ {
"checked_tryton_products": checked_tryton_products, 'checked_tryton_products': checked_tryton_products,
"failed_products": failed_products, 'failed_products': failed_products,
"updated_products": updated_products, 'updated_products': updated_products,
"created_products": created_products, 'created_products': created_products,
"untouched_products": untouched_products, 'untouched_products': untouched_products,
}, },
status=200, status=200
) )
def __get_product_datails_from_tryton(self, product_ids, tryton_client, context): def __get_product_datails_from_tryton(self, product_ids, tryton_client, context):
tryton_fields = [ tryton_fields = ['id', 'name', 'default_uom.id',
"id", 'default_uom.rec_name', 'list_price']
"name", method = 'model.product.product.read'
"default_uom.id",
"default_uom.rec_name",
"list_price",
]
method = "model.product.product.read"
params = (product_ids, tryton_fields, context) params = (product_ids, tryton_fields, context)
response = tryton_client.call(method, params) response = tryton_client.call(method, params)
return response return response
def __need_update(self, product, tryton_product): def __need_update(self, product, tryton_product):
if not product.name == tryton_product.get("name"): if not product.name == tryton_product.get('name'):
return True return True
if not product.price == tryton_product.get("list_price"): if not product.price == tryton_product.get('list_price'):
return True return True
unit = tryton_product.get("default_uom.") unit = tryton_product.get('default_uom.')
if not product.measuring_unit == unit.get("rec_name"): if not product.measuring_unit == unit.get('rec_name'):
return True return True
def __create_product(self, tryton_product): def __create_product(self, tryton_product):
product = Product() product = Product()
product.name = tryton_product.get("name") product.name = tryton_product.get('name')
product.price = tryton_product.get("list_price") product.price = tryton_product.get('list_price')
product.external_id = tryton_product.get("id") product.external_id = tryton_product.get('id')
unit = tryton_product.get("default_uom.") unit = tryton_product.get('default_uom.')
product.measuring_unit = unit.get("rec_name") product.measuring_unit = unit.get('rec_name')
product.unit_external_id = unit.get("id") product.unit_external_id = unit.get('id')
product.save() product.save()
return product return product
def __update_product(self, product, tryton_product): def __update_product(self, product, tryton_product):
product.name = tryton_product.get("name") product.name = tryton_product.get('name')
product.price = tryton_product.get("list_price") product.price = tryton_product.get('list_price')
product.external_id = tryton_product.get("id") product.external_id = tryton_product.get('id')
unit = tryton_product.get("default_uom.") unit = tryton_product.get('default_uom.')
product.measuring_unit = unit.get("rec_name") product.measuring_unit = unit.get('rec_name')
product.unit_external_id = unit.get("id") product.unit_external_id = unit.get('id')
product.save() product.save()
@@ -454,14 +384,16 @@ class CustomersFromTrytonView(APIView):
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
database=TRYTON_DATABASE, database=TRYTON_DATABASE,
username=TRYTON_USERNAME, username=TRYTON_USERNAME,
password=TRYTON_PASSWORD, password=TRYTON_PASSWORD
) )
tryton_client.connect() tryton_client.connect()
method = "model.party.party.search" method = 'model.party.party.search'
context = {"company": 1} context = {'company': 1}
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context] params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
party_ids = tryton_client.call(method, params) party_ids = tryton_client.call(method, params)
tryton_parties = self.__get_party_datails(party_ids, tryton_client, context) tryton_parties = self.__get_party_datails(
party_ids, tryton_client, context
)
checked_tryton_parties = party_ids checked_tryton_parties = party_ids
failed_parties = [] failed_parties = []
updated_customers = [] updated_customers = []
@@ -470,7 +402,9 @@ class CustomersFromTrytonView(APIView):
for tryton_party in tryton_parties: for tryton_party in tryton_parties:
try: try:
customer = Customer.objects.get(external_id=tryton_party.get("id")) customer = Customer.objects.get(
external_id=tryton_party.get('id')
)
except Customer.DoesNotExist: except Customer.DoesNotExist:
customer = self.__create_customer(tryton_party) customer = self.__create_customer(tryton_party)
created_customers.append(customer.id) created_customers.append(customer.id)
@@ -483,43 +417,41 @@ class CustomersFromTrytonView(APIView):
return Response( return Response(
{ {
"checked_tryton_parties": checked_tryton_parties, 'checked_tryton_parties': checked_tryton_parties,
"failed_parties": failed_parties, 'failed_parties': failed_parties,
"updated_customers": updated_customers, 'updated_customers': updated_customers,
"created_customers": created_customers, 'created_customers': created_customers,
"untouched_customers": untouched_customers, 'untouched_customers': untouched_customers,
}, },
status=200, status=200
) )
def __get_party_datails(self, party_ids, tryton_client, context): def __get_party_datails(self, party_ids, tryton_client, context):
tryton_fields = ["id", "name", "addresses"] tryton_fields = ['id', 'name', 'addresses']
method = "model.party.party.read" method = 'model.party.party.read'
params = (party_ids, tryton_fields, context) params = (party_ids, tryton_fields, context)
response = tryton_client.call(method, params) response = tryton_client.call(method, params)
return response return response
def __need_update(self, customer, tryton_party): def __need_update(self, customer, tryton_party):
if not customer.name == tryton_party.get("name"): if not customer.name == tryton_party.get('name'):
return True return True
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
if not customer.address_external_id == str( if not customer.address_external_id == str(tryton_party.get('addresses')[0]):
tryton_party.get("addresses")[0]
):
return True return True
def __create_customer(self, tryton_party): def __create_customer(self, tryton_party):
customer = Customer() customer = Customer()
customer.name = tryton_party.get("name") customer.name = tryton_party.get('name')
customer.external_id = tryton_party.get("id") customer.external_id = tryton_party.get('id')
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
customer.address_external_id = tryton_party.get("addresses")[0] customer.address_external_id = tryton_party.get('addresses')[0]
customer.save() customer.save()
return customer return customer
def __update_customer(self, customer, tryton_party): def __update_customer(self, customer, tryton_party):
customer.name = tryton_party.get("name") customer.name = tryton_party.get('name')
customer.external_id = tryton_party.get("id") customer.external_id = tryton_party.get('id')
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
customer.address_external_id = tryton_party.get("addresses")[0] customer.address_external_id = tryton_party.get('addresses')[0]
customer.save() customer.save()

View File

@@ -0,0 +1,66 @@
from django import forms
from django.forms.models import inlineformset_factory
from django.forms.widgets import DateInput, DateTimeInput
from .models import Sale, SaleLine, PaymentMethods
readonly_number_widget = forms.NumberInput(attrs={'readonly': 'readonly'})
class ImportProductsForm(forms.Form):
csv_file = forms.FileField()
class ImportCustomersForm(forms.Form):
csv_file = forms.FileField()
class PurchaseForm(forms.ModelForm):
class Meta:
model = Sale
fields = [
"customer",
"date",
"phone",
"description",
]
widgets = {
'date': DateInput(attrs={'type': 'date'})
}
class PurchaseLineForm(forms.ModelForm):
class Meta:
model = SaleLine
fields = [
"product",
"quantity",
"unit_price",
"description",
]
class PurchaseSummaryForm(forms.Form):
quantity_lines = forms.IntegerField(
widget=readonly_number_widget
)
quantity_products = forms.IntegerField(
widget=readonly_number_widget
)
ammount = forms.DecimalField(
max_digits=10,
decimal_places=2,
widget=readonly_number_widget
)
payment_method = forms.ChoiceField(
choices=[(PaymentMethods.CASH, PaymentMethods.CASH)],
)
SaleLineFormSet = inlineformset_factory(
Sale,
SaleLine,
extra=1,
fields='__all__'
)

View File

@@ -1,41 +0,0 @@
# Generated by Django 5.0.6 on 2026-05-28 21:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0044_alter_payment_type_payment_alter_sale_payment_method'),
]
operations = [
migrations.CreateModel(
name='CatalogSale',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(verbose_name='Date')),
('phone', models.CharField(blank=True, max_length=13, null=True)),
('description', models.CharField(blank=True, max_length=255, null=True)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='don_confiao.customer')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CatalogSaleLine',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
('unit_price', models.DecimalField(decimal_places=2, max_digits=9)),
('description', models.CharField(blank=True, max_length=255, null=True)),
('catalog_sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='don_confiao.catalogsale')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='don_confiao.product')),
],
options={
'abstract': False,
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2026-05-29 04:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0045_catalogsale_catalogsaleline'),
]
operations = [
migrations.AddField(
model_name='product',
name='active',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,210 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from decimal import Decimal
from datetime import datetime
class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Efectivo')
CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
CREDIT = 'CREDIT', _('Crédito')
class Customer(models.Model):
name = models.CharField(max_length=100, default=None, null=False, blank=False)
address = models.CharField(max_length=100, null=True, blank=True)
email = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=100, null=True, blank=True)
external_id = models.CharField(max_length=100, null=True, blank=True)
address_external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
class MeasuringUnits(models.TextChoices):
UNIT = 'UNIT', _('Unit')
class ProductCategory(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=100, unique=True)
price = models.DecimalField(max_digits=9, decimal_places=2)
measuring_unit = models.CharField(
max_length=20,
choices=MeasuringUnits.choices,
default=MeasuringUnits.UNIT
)
unit_external_id = models.CharField(max_length=100, null=True, blank=True)
categories = models.ManyToManyField(ProductCategory)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
@classmethod
def to_list(cls):
products_list = []
all_products = cls.objects.all()
for product in all_products:
rproduct = {
"id": product.id,
"name": product.name,
"price_list": product.price,
"uom": product.measuring_unit,
"categories": [c.name for c in product.categories.all()]
}
products_list.append(rproduct)
return products_list
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(max_digits=9, decimal_places=2)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
blank=False,
null=False
)
reconciliation = models.ForeignKey(
ReconciliationJar,
on_delete=models.RESTRICT,
related_name='Sales',
null=True
)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return f"{self.date} {self.customer}"
def get_total(self):
lines = self.saleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError({'payment_method': "Invalid payment method"})
@classmethod
def sale_header_csv(cls):
sale_header_csv = [field.name for field in cls._meta.fields]
return sale_header_csv
class SaleLine(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(Product, null=False, blank=False, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=10, decimal_places=2, null=True)
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
description = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
return f"{self.sale} - {self.product}"
class ReconciliationJarSummary():
def __init__(self, payments):
self._validate_payments(payments)
self._payments = payments
def _validate_payments(self, payments):
pass
@property
def total(self):
return sum([p.amount for p in self.payments])
@property
def payments(self):
return self._payments
class Payment(models.Model):
date_time = models.DateTimeField()
type_payment = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH
)
amount = models.DecimalField(max_digits=9, decimal_places=2)
reconciliation_jar = models.ForeignKey(
ReconciliationJar,
null=True,
default=None,
blank=True,
on_delete=models.RESTRICT
)
description = models.CharField(max_length=255, null=True, blank=True)
@classmethod
def get_reconciliation_jar_summary(cls):
return ReconciliationJarSummary(
cls.objects.filter(
type_payment=PaymentMethods.CASH,
reconciliation_jar=None
)
)
@classmethod
def total_payment_from_sale(cls, payment_method, sale):
payment = cls()
payment.date_time = datetime.today()
payment.type_payment = payment_method
payment.amount = sale.get_total()
payment.clean()
payment.save()
payment_sale = PaymentSale()
payment_sale.payment = payment
payment_sale.sale = sale
payment_sale.clean()
payment_sale.save()
class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

@@ -1,5 +0,0 @@
from django.db import models
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

@@ -1,17 +0,0 @@
from django.db import models
class Customer(models.Model):
name = models.CharField(
max_length=100, default=None, null=False, blank=False
)
address = models.CharField(max_length=100, null=True, blank=True)
email = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=100, null=True, blank=True)
external_id = models.CharField(max_length=100, null=True, blank=True)
address_external_id = models.CharField(
max_length=100, null=True, blank=True
)
def __str__(self):
return self.name

View File

@@ -1,38 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from datetime import datetime
class PaymentMethods(models.TextChoices):
CASH = "CASH", _("Efectivo")
CONFIAR = "CONFIAR", _("Confiar")
BANCOLOMBIA = "BANCOLOMBIA", _("Bancolombia")
CREDIT = "CREDIT", _("Crédito")
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(
max_digits=9, decimal_places=2
)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)

View File

@@ -1,48 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class MeasuringUnits(models.TextChoices):
UNIT = "UNIT", _("Unit")
class ProductCategory(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Product(models.Model):
active = models.BooleanField(default=True)
name = models.CharField(max_length=100, unique=True)
price = models.DecimalField(max_digits=9, decimal_places=2)
measuring_unit = models.CharField(
max_length=20,
choices=MeasuringUnits.choices,
default=MeasuringUnits.UNIT,
)
unit_external_id = models.CharField(
max_length=100, null=True, blank=True
)
categories = models.ManyToManyField(ProductCategory)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
@classmethod
def to_list(cls):
products_list = []
all_products = cls.objects.all()
for product in all_products:
rproduct = {
"id": product.id,
"name": product.name,
"price_list": product.price,
"uom": product.measuring_unit,
"external_id": product.external_id,
"categories": [c.name for c in product.categories.all()],
}
products_list.append(rproduct)
return products_list

View File

@@ -1,137 +0,0 @@
from django.db import models
from .customers import Customer
from .products import Product
from .payments import PaymentMethods, ReconciliationJar
from django.core.exceptions import ValidationError
from datetime import datetime
class SaleAbstractModel(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
class Meta:
abstract = True
class SaleLineAbstractModel(models.Model):
product = models.ForeignKey(
Product, null=False, blank=False, on_delete=models.CASCADE
)
quantity = models.DecimalField(
max_digits=10, decimal_places=2, null=True
)
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
description = models.CharField(max_length=255, null=True, blank=True)
class Meta:
abstract = True
class Sale(SaleAbstractModel):
payment_method = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
blank=False,
null=False,
)
reconciliation = models.ForeignKey(
ReconciliationJar,
on_delete=models.RESTRICT,
related_name="Sales",
null=True,
)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return f"{self.date} {self.customer}"
def get_total(self):
lines = self.saleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError(
{"payment_method": "Invalid payment method"}
)
@classmethod
def sale_header_csv(cls):
sale_header_csv = [field.name for field in cls._meta.fields]
return sale_header_csv
class SaleLine(SaleLineAbstractModel):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
def __str__(self):
return f"{self.sale} - {self.product}"
class CatalogSale(SaleAbstractModel):
def __str__(self):
return f"{self.date} {self.customer}"
def get_total(self):
lines = self.catalogsaleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
class CatalogSaleLine(SaleLineAbstractModel):
catalog_sale = models.ForeignKey(CatalogSale, on_delete=models.CASCADE)
def __str__(self):
return f"{self.catalog_sale} - {self.product}"
class Payment(models.Model):
date_time = models.DateTimeField()
type_payment = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
)
amount = models.DecimalField(max_digits=9, decimal_places=2)
reconciliation_jar = models.ForeignKey(
ReconciliationJar,
null=True,
default=None,
blank=True,
on_delete=models.RESTRICT,
)
description = models.CharField(max_length=255, null=True, blank=True)
@classmethod
def get_reconciliation_jar_summary(cls):
return ReconciliationJarSummary(
cls.objects.filter(
type_payment=PaymentMethods.CASH, reconciliation_jar=None
)
)
@classmethod
def total_payment_from_sale(cls, payment_method, sale):
payment = cls()
payment.date_time = datetime.today()
payment.type_payment = payment_method
payment.amount = sale.get_total()
payment.clean()
payment.save()
payment_sale = PaymentSale()
payment_sale.payment = payment
payment_sale.sale = sale
payment_sale.clean()
payment_sale.save()
class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)

View File

@@ -1,99 +1,33 @@
from rest_framework import serializers from rest_framework import serializers
from .models.sales import Sale, SaleLine from .models import Sale, SaleLine, Product, Customer, ReconciliationJar
from .models.customers import Customer
from .models.sales import (
Sale,
SaleLine,
CatalogSale,
CatalogSaleLine,
Payment,
)
from .models.products import Product, ProductCategory
from .models.payments import ReconciliationJar
class SaleLineSerializer(serializers.ModelSerializer): class SaleLineSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SaleLine model = SaleLine
fields = ["id", "sale", "product", "unit_price", "quantity"] fields = ['id', 'sale', 'product', 'unit_price', 'quantity']
class SaleSerializer(serializers.ModelSerializer): class SaleSerializer(serializers.ModelSerializer):
total = serializers.ReadOnlyField(source="get_total") total = serializers.ReadOnlyField(source='get_total')
class Meta: class Meta:
model = Sale model = Sale
fields = [ fields = ['id', 'customer', 'date', 'saleline_set',
"id", 'total', 'payment_method', 'external_id']
"customer",
"date",
"saleline_set",
"total",
"payment_method",
"external_id",
]
class CatalogSaleLineSerializer(serializers.ModelSerializer):
class Meta:
model = CatalogSaleLine
read_only_fields = ["catalog_sale"]
fields = [
"id",
"catalog_sale",
"product",
"unit_price",
"quantity",
]
class CatalogSaleSerializer(serializers.ModelSerializer):
catalogsaleline_set = CatalogSaleLineSerializer(
many=True, required=False
)
total = serializers.ReadOnlyField(source="get_total")
class Meta:
model = CatalogSale
fields = [
"id",
"customer",
"date",
"catalogsaleline_set",
"total",
]
def create(self, validated_data):
lines_data = validated_data.pop("catalogsaleline_set", [])
catalog_sale = CatalogSale.objects.create(**validated_data)
for line_data in lines_data:
CatalogSaleLine.objects.create(
catalog_sale=catalog_sale, **line_data
)
return catalog_sale
class ProductSerializer(serializers.ModelSerializer): class ProductSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Product model = Product
fields = [ fields = ['id', 'name', 'price', 'measuring_unit', 'categories']
"id",
"name",
"active",
"price",
"measuring_unit",
"categories",
"external_id",
]
class CustomerSerializer(serializers.ModelSerializer): class CustomerSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Customer model = Customer
fields = ["id", "name", "address", "email", "phone", "external_id"] fields = ['id', 'name', 'address', 'email', 'phone', 'external_id']
class ReconciliationJarSerializer(serializers.ModelSerializer): class ReconciliationJarSerializer(serializers.ModelSerializer):
@@ -102,13 +36,13 @@ class ReconciliationJarSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ReconciliationJar model = ReconciliationJar
fields = [ fields = [
"id", 'id',
"date_time", 'date_time',
"reconcilier", 'reconcilier',
"cash_taken", 'cash_taken',
"cash_discrepancy", 'cash_discrepancy',
"total_cash_purchases", 'total_cash_purchases',
"Sales", 'Sales',
] ]
@@ -118,8 +52,8 @@ class PaymentMethodSerializer(serializers.Serializer):
def to_representation(self, instance): def to_representation(self, instance):
return { return {
"text": instance[1], 'text': instance[1],
"value": instance[0], 'value': instance[0],
} }
@@ -132,8 +66,8 @@ class SaleForRenconciliationSerializer(serializers.Serializer):
def get_customer(self, sale): def get_customer(self, sale):
return { return {
"id": sale.customer.id, 'id': sale.customer.id,
"name": sale.customer.name, 'name': sale.customer.name,
} }
def get_total(self, sale): def get_total(self, sale):
@@ -143,13 +77,13 @@ class SaleForRenconciliationSerializer(serializers.Serializer):
class ListCustomerSerializer(serializers.ModelSerializer): class ListCustomerSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Customer model = Customer
fields = ["id", "name"] fields = ['id', 'name']
class ListProductSerializer(serializers.ModelSerializer): class ListProductSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Product model = Product
fields = ["id", "name"] fields = ['id', 'name']
class SummarySaleLineSerializer(serializers.ModelSerializer): class SummarySaleLineSerializer(serializers.ModelSerializer):
@@ -157,13 +91,13 @@ class SummarySaleLineSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SaleLine model = SaleLine
fields = ["product", "quantity", "unit_price", "description"] fields = ['product', 'quantity', 'unit_price', 'description']
class SaleSummarySerializer(serializers.ModelSerializer): class SaleSummarySerializer(serializers.ModelSerializer):
customer = ListCustomerSerializer() customer = ListCustomerSerializer()
lines = SummarySaleLineSerializer(many=True, source="saleline_set") lines = SummarySaleLineSerializer(many=True, source='saleline_set')
class Meta: class Meta:
model = Sale model = Sale
fields = ["id", "date", "customer", "payment_method", "lines"] fields = ['id', 'date', 'customer', 'payment_method', 'lines']

View File

@@ -0,0 +1,17 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<title>Don Confiao - Tienda la Ilusión</title>
</head>
<body class="flex h-full w-full">
<div id="menu" class="h-full w-2/12 border bg-green-400 max-h-screen overflow-auto">
{% include 'don_confiao/menu.html' %}
</div>
<div id="content" class="w-10/12 h-screen max-h-full overflow-auto">
{% block content %} {% endblock %}
</div>
</body>
<script src="https://cdn.tailwindcss.com/"></script>
</html>

View File

@@ -0,0 +1,13 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post">
{% else %}
<form method="post">
{% endif %}
{% csrf_token %}
{{ form }}
<input type="submit" value="Importar">
</form>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post">
{% else %}
<form method="post">
{% endif %}
{% csrf_token %}
{{ form }}
<input type="submit" value="Importar">
</form>
{% endblock %}

View File

@@ -0,0 +1,8 @@
<h1>Tienda la Ilusión</h1>
<h2>Don Confiao</h2>
<ul>
<li><a href='./comprar'>Comprar</a></li>
<li><a href='./productos'>Productos</a></li>
<li><a href='./importar_productos'>Importar Productos</a></li>
<li><a href='./importar_terceros'>Importar Terceros</a></li>
</ul>

View File

@@ -0,0 +1,17 @@
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/main_menu.css' %}">
<div class="h-full flex flex-col justify-around shadow hover:shadow-lg">
<img class="w-full px-12" src="{% static 'img/recreo_logo.png' %}" alt="Recreo">
<nav id="main_menu">
<ul class="flex flex-col m-0 p-0 justify-center shadow hover:shadow-lg gap-y-12 items-center drop-shadow-lg">
<li><a href='/don_confiao/comprar' >Comprar</a></li>
<li><a href='/don_confiao/compras'>Compras</a></li>
<li><a href='/don_confiao/lista_productos'>Productos</a></li>
<li><a href='/don_confiao/importar_productos'>Importar Productos</a></li>
<li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li>
</ul>
</nav>
<p id="page_title" class="text-center decoration-solid font-mono font-bold text-lg page_title">Don Confiao - Tienda la Ilusión</p>
</div>
<script src="https://cdn.tailwindcss.com/"></script>

View File

@@ -0,0 +1,14 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
<form action="" method="get">
<label>Filtro por nombre:</label>
<input type="text" name="name" value="{{ request.GET.name }}">
<button type="submit">Filtrar</button>
</form>
<h1>Lista de productos</h1>
<ul>
{% for obj in object_list %}
<li>{{ obj.name }} ({{ obj.id }})</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% load static %}
<script>
let listProducts = JSON.parse("{{ list_products|escapejs }}");
</script>
<div class="flex h-full">
<div class="h-full w-10/12 flex flex-col p-5">
<form id="complete_form_purchase" method="POST" class="h-10/12 w-full max-h-full overflow-auto">
{% csrf_token %}
{{ linea_formset.management_form }}
<div id="formset-container" class="w-full">
{% for form in linea_formset %}
<div class="form-container flex justify-center ">
<table class="w-3/4 my-5 shadow-inner" style="border: solid 1px #178E79;">
{{ form.as_table }}
</table>
</div>
{% endfor %}
</div>
<div class="h-2/12 flex justify-center">
<button id="add_line" type="button" class="bg-yellow-400 shadow hover:shadow-lg py-2 px-5 rounded-full font-bold hover:bg-violet-200 ease-in duration-150">Añadir Linea</button>
</div>
</div>
<div class="h-full w-3/12 bg-green-400 p-5 shadow hover:shadow-lg flex flex-col gap-y-3 font-semibold justify-around">
<p id="sale_resume_title" class="text-center decoration-solid font-mono font-bold text-xl page_title">Resumen de Venta</p>
{{ sale_form }}
{{ summary_form }}
<button class="font-bold my-10 py-2 px-4 rounded-full bg-yellow-400 shadow hover:shadow-lg hover:bg-violet-200 ease-in duration-150" name="form" type="submit" >Comprar</button>
</div>
</form>
</div>
<script src="https://cdn.tailwindcss.com/"></script>
<script src="{% static 'js/buy_general.js' %}"></script>
<script src="{% static 'js/add_line.js' %}"></script>
<script src="{% static 'js/sale_summary.js' %}"></script>
<script src="{% static 'js/calculate_subtotal_line.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
<h1>Resumen de compra</h1>
<dl>
<dt>Date</dt> <dd>{{ purchase.date }}</dd>
<dt>ID</dt> <dd>{{ purchase.id }}</dd>
<dt>Customer</dt> <dd>{{ purchase.customer.name }}</dd>
<dt>Total</dt> <dd>{{ purchase.get_total }}</dd>
</dl>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if purchases %}
<ul>
{% for purchase in purchases %}
<li><a href="/don_confiao/resumen_compra/{{ purchase.id }}">{{ purchase.date }}, {{ purchase.customer }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No hay Compras</p>
{% endif %}
{% endblock %}

View File

@@ -1,6 +1,6 @@
from django.test import TestCase from django.test import TestCase
from ..models.admin import AdminCode from ..models import AdminCode
from .Mixins import LoginMixin from .Mixins import LoginMixin
import json import json
@@ -10,33 +10,33 @@ class TestAdminCode(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login() self.login()
self.valid_code = "some valid code" self.valid_code = 'some valid code'
admin_code = AdminCode() admin_code = AdminCode()
admin_code.value = self.valid_code admin_code.value = self.valid_code
admin_code.clean() admin_code.clean()
admin_code.save() admin_code.save()
def test_validate_code(self): def test_validate_code(self):
url = "/don_confiao/api/admin_code/validate/" + self.valid_code url = '/don_confiao/api/admin_code/validate/' + self.valid_code
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
self.assertTrue(content["validCode"]) self.assertTrue(content['validCode'])
def test_invalid_code(self): def test_invalid_code(self):
invalid_code = "some invalid code" invalid_code = 'some invalid code'
url = "/don_confiao/api/admin_code/validate/" + invalid_code url = '/don_confiao/api/admin_code/validate/' + invalid_code
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
self.assertFalse(content["validCode"]) self.assertFalse(content['validCode'])
def test_empty_code(self): def test_empty_code(self):
empty_code = "" empty_code = ''
url = "/don_confiao/api/admin_code/validate/" + empty_code url = '/don_confiao/api/admin_code/validate/' + empty_code
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View File

@@ -4,9 +4,7 @@ import io
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from ..models.sales import Sale, CatalogSale from ..models import Sale, Product, Customer
from ..models.customers import Customer
from ..models.products import Product
from .Mixins import LoginMixin from .Mixins import LoginMixin
@@ -15,13 +13,18 @@ class TestAPI(APITestCase, LoginMixin):
self.login() self.login()
self.product = Product.objects.create( self.product = Product.objects.create(
name="Panela", price=5000, measuring_unit="UNIT" name='Panela',
price=5000,
measuring_unit='UNIT'
)
self.customer = Customer.objects.create(
name='Camilo',
external_id='18'
) )
self.customer = Customer.objects.create(name="Camilo", external_id="18")
def test_create_sale(self): def test_create_sale(self):
response = self._create_sale() response = self._create_sale()
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1) self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0] sale = Sale.objects.all()[0]
@@ -29,71 +32,79 @@ class TestAPI(APITestCase, LoginMixin):
sale.customer.name, sale.customer.name,
self.customer.name, self.customer.name,
) )
self.assertEqual(sale.id, content["id"]) self.assertEqual(
sale.id,
content['id']
)
self.assertIsNone(sale.external_id) self.assertIsNone(sale.external_id)
def test_create_catalog_sale(self):
response = self._create_catalog_sale()
content = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(CatalogSale.objects.count(), 1)
sale = CatalogSale.objects.all()[0]
self.assertEqual(sale.customer.name, self.customer.name)
self.assertEqual(sale.id, content["id"])
self.assertEqual(sale.catalogsaleline_set.count(), 2)
def test_create_sale_with_decimal(self): def test_create_sale_with_decimal(self):
response = self._create_sale_with_decimal() response = self._create_sale_with_decimal()
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1) self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0] sale = Sale.objects.all()[0]
self.assertEqual(sale.customer.name, self.customer.name) self.assertEqual(
self.assertEqual(sale.id, content["id"]) sale.customer.name,
self.assertEqual(sale.get_total(), 16500.00) self.customer.name
)
self.assertEqual(
sale.id,
content['id']
)
self.assertEqual(
sale.get_total(),
16500.00
)
def test_get_products(self): def test_get_products(self):
url = "/don_confiao/api/products/" url = '/don_confiao/api/products/'
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode("utf-8")) json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.product.name, json_response[0]["name"]) self.assertEqual(self.product.name, json_response[0]['name'])
def test_get_customers(self): def test_get_customers(self):
url = "/don_confiao/api/customers/" url = '/don_confiao/api/customers/'
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode("utf-8")) json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.name, json_response[0]["name"]) self.assertEqual(self.customer.name, json_response[0]['name'])
self.assertEqual(self.customer.external_id, json_response[0]["external_id"]) self.assertEqual(
self.customer.external_id,
json_response[0]['external_id']
)
def test_get_sales(self): def test_get_sales(self):
url = "/don_confiao/api/sales/" url = '/don_confiao/api/sales/'
self._create_sale() self._create_sale()
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode("utf-8")) json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.id, json_response[0]["customer"]) self.assertEqual(self.customer.id, json_response[0]['customer'])
self.assertEqual(None, json_response[0]["external_id"]) self.assertEqual(
None,
json_response[0]['external_id']
)
def test_get_sales_for_tryton(self): def test_get_sales_for_tryton(self):
url = "/don_confiao/api/sales/for_tryton" url = '/don_confiao/api/sales/for_tryton'
self._create_sale() self._create_sale()
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode("utf-8")) json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("csv", json_response) self.assertIn('csv', json_response)
self.assertGreater(len(json_response["csv"]), 0) self.assertGreater(len(json_response['csv']), 0)
def test_csv_structure_in_sales_for_tryton(self): def test_csv_structure_in_sales_for_tryton(self):
url = "/don_confiao/api/sales/for_tryton" url = '/don_confiao/api/sales/for_tryton'
self._create_sale() self._create_sale()
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode("utf-8")) json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
csv_reader = csv.reader(io.StringIO(json_response["csv"])) csv_reader = csv.reader(io.StringIO(json_response['csv']))
expected_header = [ expected_header = [
"Tercero", "Tercero",
"Dirección de facturación", "Dirección de facturación",
@@ -112,113 +123,53 @@ class TestAPI(APITestCase, LoginMixin):
"Tienda", "Tienda",
"Terminal de venta", "Terminal de venta",
"Autorecogida", "Autorecogida",
"Comentario", "Comentario"
] ]
self.assertEqual(next(csv_reader), expected_header) self.assertEqual(next(csv_reader), expected_header)
expected_rows = [ expected_rows = [
[ [self.customer.name, self.customer.name, self.customer.name, "",
self.customer.name, "", "2024-09-02", "Contado", "Almacén",
self.customer.name, "Peso colombiano", self.product.name, "2.00", "3000.00", "Unidad",
self.customer.name, "TIENDA LA ILUSIÓN", "Tienda La Ilusion", "La Ilusion", "True", ""
"", ],
"", ["", "", "", "", "", "", "", "", "", self.product.name, "3.00",
"2024-09-02", "5000.00", "Unidad", "", "", "", "", ""
"Contado", ],
"Almacén",
"Peso colombiano",
self.product.name,
"2.00",
"3000.00",
"Unidad",
"TIENDA LA ILUSIÓN",
"Tienda La Ilusion",
"La Ilusion",
"True",
"",
],
[
"",
"",
"",
"",
"",
"",
"",
"",
"",
self.product.name,
"3.00",
"5000.00",
"Unidad",
"",
"",
"",
"",
"",
],
] ]
rows = list(csv_reader) rows = list(csv_reader)
self.assertEqual(rows, expected_rows) self.assertEqual(rows, expected_rows)
def _create_sale(self): def _create_sale(self):
url = "/don_confiao/api/sales/" url = '/don_confiao/api/sales/'
data = { data = {
"customer": self.customer.id, 'customer': self.customer.id,
"date": "2024-09-02", 'date': '2024-09-02',
"payment_method": "CASH", 'payment_method': 'CASH',
"saleline_set": [ 'saleline_set': [
{ {'product': self.product.id, 'quantity': 2, 'unit_price': 3000},
"product": self.product.id, {'product': self.product.id, 'quantity': 3, 'unit_price': 5000}
"quantity": 2,
"unit_price": 3000,
},
{
"product": self.product.id,
"quantity": 3,
"unit_price": 5000,
},
], ],
} }
return self.client.post(url, data, format="json") return self.client.post(url, data, format='json')
def _create_catalog_sale(self):
url = "/don_confiao/api/catalog_sales/"
data = {
"customer": self.customer.id,
"date": "2024-09-02",
"catalogsaleline_set": [
{
"product": self.product.id,
"quantity": 2,
"unit_price": 3000,
},
{
"product": self.product.id,
"quantity": 3,
"unit_price": 5000,
},
],
}
return self.client.post(url, data, format="json")
def _create_sale_with_decimal(self): def _create_sale_with_decimal(self):
url = "/don_confiao/api/sales/" url = '/don_confiao/api/sales/'
data = { data = {
"customer": self.customer.id, 'customer': self.customer.id,
"date": "2024-09-02", 'date': '2024-09-02',
"payment_method": "CASH", 'payment_method': 'CASH',
"saleline_set": [ 'saleline_set': [
{ {
"product": self.product.id, 'product': self.product.id,
"quantity": 0.5, 'quantity': 0.5,
"unit_price": 3000, 'unit_price': 3000
}, },
{ {
"product": self.product.id, 'product': self.product.id,
"quantity": 3, 'quantity': 3,
"unit_price": 5000, 'unit_price': 5000
}, }
], ],
} }
return self.client.post(url, data, format="json") return self.client.post(url, data, format='json')

View File

@@ -0,0 +1,22 @@
from django.test import Client, TestCase
from ..models import Product
class TestBuyForm(TestCase):
def setUp(self):
self.client = Client()
self.product = Product()
self.product.name = "Arroz"
self.product.price = 5000
self.product.save()
def test_buy_contains_products_list(self):
response = self.client.get('/don_confiao/comprar')
self.assertIn(
self.product.name,
response.context['list_products']
)
content = response.content.decode('utf-8')
self.assertIn('5000', content)
self.assertIn('Arroz', content)
self.assertIn(str(self.product.id), content)

View File

@@ -2,7 +2,7 @@ import json
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from ..models.customers import Customer from ..models import Customer
from .Mixins import LoginMixin from .Mixins import LoginMixin
@@ -11,71 +11,61 @@ class TestCustomersFromTryton(TestCase, LoginMixin):
self.login() self.login()
self.customer = Customer.objects.create( self.customer = Customer.objects.create(
name="Calos", external_id=5 name='Calos',
external_id=5
) )
self.customer.save() self.customer.save()
self.customer2 = Customer.objects.create( self.customer2 = Customer.objects.create(
name="Cristian", external_id=6 name='Cristian',
external_id=6
) )
self.customer2.save() self.customer2.save()
@patch("sabatron_tryton_rpc_client.client.Client.call") @patch('sabatron_tryton_rpc_client.client.Client.call')
@patch("sabatron_tryton_rpc_client.client.Client.connect") @patch('sabatron_tryton_rpc_client.client.Client.connect')
def test_create_import_customer(self, mock_connect, mock_call): def test_create_import_customer(self, mock_connect, mock_call):
def fake_call(*args, **kwargs): def fake_call(*args, **kwargs):
party_search = "model.party.party.search" party_search = 'model.party.party.search'
search_args = [ search_args = [[], 0, 1000, [['name', 'ASC'], ['id', None]], {'company': 1}]
[],
0,
1000,
[["name", "ASC"], ["id", None]],
{"company": 1},
]
if args == (party_search, search_args): if (args == (party_search, search_args)):
return [5, 6, 7, 8] return [5, 6, 7, 8]
party_read = "model.party.party.read" party_read = 'model.party.party.read'
read_args = ( read_args = ([5, 6, 7, 8], ['id', 'name', 'addresses'], {'company': 1})
[5, 6, 7, 8], if (args == (party_read, read_args)):
["id", "name", "addresses"],
{"company": 1},
)
if args == (party_read, read_args):
return [ return [
{"id": 5, "name": "Carlos", "addresses": [303]}, {'id': 5, 'name': 'Carlos', 'addresses': [303]},
{"id": 6, "name": "Cristian", "addresses": []}, {'id': 6, 'name': 'Cristian', 'addresses': []},
{"id": 7, "name": "Ana", "addresses": [302]}, {'id': 7, 'name': 'Ana', 'addresses': [302]},
{"id": 8, "name": "José", "addresses": []}, {'id': 8, 'name': 'José', 'addresses': []},
] ]
raise Exception( raise Exception(f"Sorry, args non expected on this test: {args}")
f"Sorry, args non expected on this test: {args}"
)
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
url = "/don_confiao/api/importar_clientes_de_tryton" url = '/don_confiao/api/importar_clientes_de_tryton'
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
content = json.loads(response.content.decode('utf-8'))
expected_response = { expected_response = {
"checked_tryton_parties": [5, 6, 7, 8], 'checked_tryton_parties': [5, 6, 7, 8],
"created_customers": [3, 4], 'created_customers': [3, 4],
"untouched_customers": [2], 'untouched_customers': [2],
"failed_parties": [], 'failed_parties': [],
"updated_customers": [1], 'updated_customers': [1]
} }
self.assertEqual(content, expected_response) self.assertEqual(content, expected_response)
created_customer = Customer.objects.get(id=3) created_customer = Customer.objects.get(id=3)
self.assertEqual(created_customer.external_id, str(7)) self.assertEqual(created_customer.external_id, str(7))
self.assertEqual(created_customer.name, "Ana") self.assertEqual(created_customer.name, 'Ana')
self.assertEqual(created_customer.address_external_id, str(302)) self.assertEqual(created_customer.address_external_id, str(302))
updated_customer = Customer.objects.get(id=1) updated_customer = Customer.objects.get(id=1)
self.assertEqual(updated_customer.external_id, str(5)) self.assertEqual(updated_customer.external_id, str(5))
self.assertEqual(updated_customer.name, "Carlos") self.assertEqual(updated_customer.name, 'Carlos')
self.assertIn(updated_customer.address_external_id, str(303)) self.assertIn(updated_customer.address_external_id, str(303))

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
from django.test import Client, TestCase
from io import StringIO
import csv
class TestExportSales(TestCase):
fixtures = ['sales_fixture']
def setUp(self):
self.client = Client()
def test_export_sales(self):
sales_response = self._export_sales_csv()
filename = sales_response.headers[
'Content-Disposition'].split('; ')[1].strip('filename=').strip("'")
content = sales_response.content
content_str = content.decode('utf-8')
csv_file = StringIO(content_str)
header = next(csv.reader(csv_file))
self.assertGreater(len(content), 0)
self.assertEqual(filename, 'sales.csv')
self.assertEqual(sales_response.headers['Content-Type'], 'text/csv')
self.assertEqual(header, self._tryton_sale_header())
def _export_sales_csv(self):
return self.client.get("/don_confiao/exportar_ventas_para_tryton")
def _tryton_sale_header(self):
return [
"Tercero",
"Dirección de facturación",
"Dirección de envío",
"Descripción",
"Referencia",
"Fecha venta",
"Plazo de pago",
"Almacén",
"Moneda",
"Líneas/Producto",
"Líneas/Cantidad",
"Líneas/Precio unitario",
"Líneas/Unidad",
"Empresa",
"Tienda",
"Terminal de venta",
"Autorecogida",
"Comentario"
]

View File

@@ -0,0 +1,105 @@
import csv
import json
from unittest.mock import patch
from django.test import TestCase
from ..models import Sale, SaleLine, Product, Customer
from .Mixins import LoginMixin
class TestExportarVentasParaTryton(TestCase, LoginMixin):
def setUp(self):
self.login()
self.product = Product.objects.create(
name='Panela',
price=5000,
measuring_unit='UNIT',
unit_external_id=1,
external_id=1
)
self.customer = Customer.objects.create(
name='Camilo',
external_id=1,
address_external_id=307,
)
self.sale = Sale.objects.create(
customer=self.customer,
date='2024-09-02',
payment_method='CASH',
description='un comentario'
)
self.sale_line1 = SaleLine.objects.create(
product=self.product,
quantity=2,
unit_price=3000,
sale=self.sale
)
self.sale_line2 = SaleLine.objects.create(
product=self.product,
quantity=3,
unit_price=5000,
sale=self.sale
)
def test_exportar_ventas_para_tryton(self):
url = '/don_confiao/exportar_ventas_para_tryton'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'text/csv')
csv_content = response.content.decode('utf-8')
expected_header = [
"Tercero",
"Dirección de facturación",
"Dirección de envío",
"Descripción",
"Referencia",
"Fecha venta",
"Plazo de pago",
"Almacén",
"Moneda",
"Líneas/Producto",
"Líneas/Cantidad",
"Líneas/Precio unitario",
"Líneas/Unidad",
"Empresa",
"Tienda",
"Terminal de venta",
"Autorecogida",
"Comentario"
]
csv_reader = csv.reader(csv_content.splitlines())
self.assertEqual(next(csv_reader), expected_header)
expected_rows = [
["Camilo", "Camilo", "Camilo", "un comentario", "un comentario", "2024-09-02", "Contado", "Almacén", "Peso colombiano", "Panela", "2.00", "3000.00", "Unidad", "TIENDA LA ILUSIÓN", "Tienda La Ilusion", "La Ilusion", "True", "un comentario"],
["", "", "", "", "", "", "", "", "", "Panela", "3.00", "5000.00", "Unidad", "", "", "", "", ""],
]
csv_rows = list(csv_reader)
self.assertEqual(csv_rows[0], expected_rows[0])
self.assertEqual(csv_rows[1], expected_rows[1])
@patch('sabatron_tryton_rpc_client.client.Client.call')
@patch('sabatron_tryton_rpc_client.client.Client.connect')
def test_send_sales_to_tryton(self, mock_connect, mock_call):
external_id = '23423'
url = '/don_confiao/api/enviar_ventas_a_tryton'
mock_connect.return_value = None
mock_call.return_value = [external_id]
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
expected_response = {
'successful': [self.sale.id],
'failed': [],
}
self.assertEqual(content, expected_response)
updated_sale = Sale.objects.get(id=self.sale.id)
self.assertEqual(updated_sale.external_id, external_id)
mock_connect.assert_called_once()
mock_call.assert_called_once()
mock_call.assert_called_with('model.sale.sale.create', [[{'company': 1, 'shipment_address': '307', 'invoice_address': '307', 'currency': 31, 'comment': 'un comentario', 'description': 'Metodo pago: CASH', 'party': '1', 'reference': 'don_confiao 1', 'sale_date': {'__class__': 'date', 'year': 2024, 'month': 9, 'day': 2}, 'lines': [['create', [{'product': '1', 'quantity': {'__class__': 'Decimal', 'decimal': '2.00'}, 'type': 'line', 'unit': '1', 'unit_price': {'__class__': 'Decimal', 'decimal': '3000.00'}}, {'product': '1', 'quantity': {'__class__': 'Decimal', 'decimal': '3.00'}, 'type': 'line', 'unit': '1', 'unit_price': {'__class__': 'Decimal', 'decimal': '5000.00'}}]]], 'self_pick_up': True}], {'company': 1, 'shops': [1]}])

View File

@@ -1,12 +1,6 @@
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ..models.sales import ( from ..models import Sale, Product, SaleLine, Customer, ReconciliationJar
Sale,
Product,
SaleLine,
Customer,
ReconciliationJar,
)
from .Mixins import LoginMixin from .Mixins import LoginMixin
import json import json
@@ -17,13 +11,13 @@ class TestJarReconcliation(TestCase, LoginMixin):
self.login() self.login()
customer = Customer() customer = Customer()
customer.name = "Alejo Mono" customer.name = 'Alejo Mono'
customer.save() customer.save()
purchase = Sale() purchase = Sale()
purchase.customer = customer purchase.customer = customer
purchase.date = "2024-07-30" purchase.date = "2024-07-30"
purchase.payment_method = "CASH" purchase.payment_method = 'CASH'
purchase.clean() purchase.clean()
purchase.save() purchase.save()
@@ -43,7 +37,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase2 = Sale() purchase2 = Sale()
purchase2.customer = customer purchase2.customer = customer
purchase2.date = "2024-07-30" purchase2.date = "2024-07-30"
purchase.payment_method = "CASH" purchase.payment_method = 'CASH'
purchase2.clean() purchase2.clean()
purchase2.save() purchase2.save()
@@ -58,7 +52,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase3 = Sale() purchase3 = Sale()
purchase3.customer = customer purchase3.customer = customer
purchase3.date = "2024-07-30" purchase3.date = "2024-07-30"
purchase3.payment_method = "CASH" purchase3.payment_method = 'CASH'
purchase3.clean() purchase3.clean()
purchase3.save() purchase3.save()
@@ -73,7 +67,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase4 = Sale() purchase4 = Sale()
purchase4.customer = customer purchase4.customer = customer
purchase4.date = "2024-07-30" purchase4.date = "2024-07-30"
purchase4.payment_method = "CONFIAR" purchase4.payment_method = 'CONFIAR'
purchase4.clean() purchase4.clean()
purchase4.save() purchase4.save()
@@ -96,19 +90,19 @@ class TestJarReconcliation(TestCase, LoginMixin):
self.purchase3.clean() self.purchase3.clean()
self.purchase3.save() self.purchase3.save()
url = "/don_confiao/purchases/for_reconciliation" url = '/don_confiao/purchases/for_reconciliation'
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
rawContent = response.content.decode("utf-8") rawContent = response.content.decode('utf-8')
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertIn("CASH", content.keys()) self.assertIn('CASH', content.keys())
self.assertIn("CONFIAR", content.keys()) self.assertIn('CONFIAR', content.keys())
self.assertEqual(2, len(content.get("CASH"))) self.assertEqual(2, len(content.get('CASH')))
self.assertEqual(1, len(content.get("CONFIAR"))) self.assertEqual(1, len(content.get('CONFIAR')))
self.assertNotIn(str(37 * 72500), rawContent) self.assertNotIn(str(37*72500), rawContent)
self.assertIn(str(47 * 72500), rawContent) self.assertIn(str(47*72500), rawContent)
def test_don_create_reconcialiation_with_bad_numbers(self): def test_don_create_reconcialiation_with_bad_numbers(self):
reconciliation = ReconciliationJar() reconciliation = ReconciliationJar()
@@ -120,137 +114,131 @@ class TestJarReconcliation(TestCase, LoginMixin):
reconciliation.clean() reconciliation.clean()
reconciliation.save() reconciliation.save()
def test_fail_create_reconciliation_with_wrong_total_purchases_purchases( def test_fail_create_reconciliation_with_wrong_total_purchases_purchases(self):
self, url = '/don_confiao/reconciliate_jar'
):
url = "/don_confiao/reconciliate_jar"
total_purchases = (11 * 72500) + (27 * 72500) total_purchases = (11 * 72500) + (27 * 72500)
bad_total_purchases = total_purchases + 2 bad_total_purchases = total_purchases + 2
data = { data = {
"date_time": "2024-12-02T21:07", 'date_time': '2024-12-02T21:07',
"reconcilier": "carlos", 'reconcilier': 'carlos',
"total_cash_purchases": bad_total_purchases, 'total_cash_purchases': bad_total_purchases,
"cash_taken": total_purchases, 'cash_taken': total_purchases,
"cash_discrepancy": 0, 'cash_discrepancy': 0,
"cash_purchases": [ 'cash_purchases': [
self.purchase.id, self.purchase.id,
self.purchase2.id, self.purchase2.id,
self.purchase.id, self.purchase.id,
], ],
} }
response = self.client.post( response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
url, content_type='application/json')
data=json.dumps(data).encode("utf-8"), rawContent = response.content.decode('utf-8')
content_type="application/json",
)
rawContent = response.content.decode("utf-8")
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIn("error", content) self.assertIn('error', content)
self.assertIn("total_cash_purchases", content["error"]) self.assertIn('total_cash_purchases', content['error'])
def test_create_reconciliation_with_purchases(self): def test_create_reconciliation_with_purchases(self):
response = self._create_reconciliation_with_purchase() response = self._create_reconciliation_with_purchase()
rawContent = response.content.decode("utf-8") rawContent = response.content.decode('utf-8')
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("id", content) self.assertIn('id', content)
purchases = Sale.objects.filter(reconciliation_id=content["id"]) purchases = Sale.objects.filter(reconciliation_id=content['id'])
self.assertEqual(len(purchases), 2) self.assertEqual(len(purchases), 2)
def test_create_reconciliation_with_purchases_and_other_totals(self): def test_create_reconciliation_with_purchases_and_other_totals(self):
url = "/don_confiao/reconciliate_jar" url = '/don_confiao/reconciliate_jar'
total_purchases = (11 * 72500) + (27 * 72500) total_purchases = (11 * 72500) + (27 * 72500)
data = { data = {
"date_time": "2024-12-02T21:07", 'date_time': '2024-12-02T21:07',
"reconcilier": "carlos", 'reconcilier': 'carlos',
"total_cash_purchases": total_purchases, 'total_cash_purchases': total_purchases,
"cash_taken": total_purchases, 'cash_taken': total_purchases,
"cash_discrepancy": 0, 'cash_discrepancy': 0,
"cash_purchases": [ 'cash_purchases': [
self.purchase.id, self.purchase.id,
self.purchase2.id, self.purchase2.id,
], ],
"other_totals": { 'other_totals': {
"Confiar": { 'Confiar': {
"total": (47 * 72500) + 1, 'total': (47 * 72500) + 1,
"purchases": [self.purchase4.id], 'purchases': [self.purchase4.id],
}, },
}, },
} }
response = self.client.post( response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
url, content_type='application/json')
data=json.dumps(data).encode("utf-8"),
content_type="application/json",
)
rawContent = response.content.decode("utf-8") rawContent = response.content.decode('utf-8')
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("id", content) self.assertIn('id', content)
purchases = Sale.objects.filter(reconciliation_id=content["id"]) purchases = Sale.objects.filter(reconciliation_id=content['id'])
self.assertEqual(len(purchases), 3) self.assertEqual(len(purchases), 3)
def test_list_reconciliations(self): def test_list_reconciliations(self):
self._create_simple_reconciliation() self._create_simple_reconciliation()
self._create_simple_reconciliation() self._create_simple_reconciliation()
url = "/don_confiao/api/reconciliate_jar/" url = '/don_confiao/api/reconciliate_jar/'
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
self.assertEqual(2, content["count"]) self.assertEqual(2, content['count'])
self.assertEqual(2, len(content["results"])) self.assertEqual(2, len(content['results']))
self.assertEqual( self.assertEqual('2024-07-30T00:00:00Z',
"2024-07-30T00:00:00Z", content["results"][0]["date_time"] content['results'][0]['date_time'])
)
def test_list_reconciliations_pagination(self): def test_list_reconciliations_pagination(self):
self._create_simple_reconciliation() self._create_simple_reconciliation()
self._create_simple_reconciliation() self._create_simple_reconciliation()
url = "/don_confiao/api/reconciliate_jar/?page=2&page_size=1" url = '/don_confiao/api/reconciliate_jar/?page=2&page_size=1'
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
self.assertEqual(1, len(content["results"])) self.assertEqual(1, len(content['results']))
self.assertEqual( self.assertEqual('2024-07-30T00:00:00Z',
"2024-07-30T00:00:00Z", content["results"][0]["date_time"] content['results'][0]['date_time'])
)
def test_get_single_reconciliation(self): def test_get_single_reconciliation(self):
createResponse = self._create_reconciliation_with_purchase() createResponse = self._create_reconciliation_with_purchase()
reconciliationId = json.loads( reconciliationId = json.loads(
createResponse.content.decode("utf-8") createResponse.content.decode('utf-8')
)["id"] )['id']
self.assertGreater(reconciliationId, 0) self.assertGreater(reconciliationId, 0)
url = f"/don_confiao/api/reconciliate_jar/{reconciliationId}/" url = f'/don_confiao/api/reconciliate_jar/{reconciliationId}/'
response = self.client.get(url, content_type="application/json") response = self.client.get(url, content_type='application/json')
content = json.loads(response.content.decode("utf-8")) content = json.loads(
self.assertEqual(reconciliationId, content["id"]) response.content.decode('utf-8')
self.assertGreater(len(content["Sales"]), 0) )
self.assertEqual(reconciliationId, content['id'])
self.assertGreater(len(content['Sales']), 0)
self.assertIn( self.assertIn(
self.purchase.id, [sale["id"] for sale in content["Sales"]] self.purchase.id,
[sale['id'] for sale in content['Sales']]
) )
self.assertIn( self.assertIn(
"CASH", [sale["payment_method"] for sale in content["Sales"]] 'CASH',
[sale['payment_method'] for sale in content['Sales']]
) )
def test_create_reconciliation_with_decimal_on_sale_lines(self): def test_create_reconciliation_with_decimal_on_sale_lines(self):
customer = Customer() customer = Customer()
customer.name = "Consumidor final" customer.name = 'Consumidor final'
customer.save() customer.save()
product = Product() product = Product()
@@ -261,7 +249,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase = Sale() purchase = Sale()
purchase.customer = customer purchase.customer = customer
purchase.date = "2024-07-30" purchase.date = "2024-07-30"
purchase.payment_method = "CASH" purchase.payment_method = 'CASH'
purchase.clean() purchase.clean()
purchase.save() purchase.save()
@@ -272,23 +260,23 @@ class TestJarReconcliation(TestCase, LoginMixin):
line.unit_price = "57.50" line.unit_price = "57.50"
line.save() line.save()
url = "/don_confiao/reconciliate_jar" url = '/don_confiao/reconciliate_jar'
total_purchases = 13.80 total_purchases = 13.80
data = { data = {
"date_time": "2024-12-02T21:07", 'date_time': '2024-12-02T21:07',
"reconcilier": "carlos", 'reconcilier': 'carlos',
"total_cash_purchases": total_purchases, 'total_cash_purchases': total_purchases,
"cash_taken": total_purchases, 'cash_taken': total_purchases,
"cash_discrepancy": 0, 'cash_discrepancy': 0,
"cash_purchases": [purchase.id], 'cash_purchases': [purchase.id],
} }
response = self.client.post( response = self.client.post(
url, url, data=json.dumps(data).encode('utf-8'),
data=json.dumps(data).encode("utf-8"), content_type='application/json'
content_type="application/json",
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def _create_simple_reconciliation(self): def _create_simple_reconciliation(self):
reconciliation = ReconciliationJar() reconciliation = ReconciliationJar()
reconciliation.date_time = "2024-07-30" reconciliation.date_time = "2024-07-30"
@@ -300,22 +288,19 @@ class TestJarReconcliation(TestCase, LoginMixin):
return reconciliation return reconciliation
def _create_reconciliation_with_purchase(self): def _create_reconciliation_with_purchase(self):
url = "/don_confiao/reconciliate_jar" url = '/don_confiao/reconciliate_jar'
total_purchases = (11 * 72500) + (27 * 72500) total_purchases = (11 * 72500) + (27 * 72500)
data = { data = {
"date_time": "2024-12-02T21:07", 'date_time': '2024-12-02T21:07',
"reconcilier": "carlos", 'reconcilier': 'carlos',
"total_cash_purchases": total_purchases, 'total_cash_purchases': total_purchases,
"cash_taken": total_purchases, 'cash_taken': total_purchases,
"cash_discrepancy": 0, 'cash_discrepancy': 0,
"cash_purchases": [ 'cash_purchases': [
self.purchase.id, self.purchase.id,
self.purchase2.id, self.purchase2.id,
self.purchase.id, self.purchase.id,
], ],
} }
return self.client.post( return self.client.post(url, data=json.dumps(data).encode('utf-8'),
url, content_type='application/json')
data=json.dumps(data).encode("utf-8"),
content_type="application/json",
)

View File

@@ -2,7 +2,7 @@
from django.test import TestCase from django.test import TestCase
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from ..models.customers import Customer from ..models import Customer
class TestCustomer(TestCase): class TestCustomer(TestCase):

View File

@@ -1,9 +1,6 @@
from django.test import Client, TestCase from django.test import Client, TestCase
from django.conf import settings from django.conf import settings
from rest_framework.test import APITestCase from ..models import ProductCategory, Product
from rest_framework import status
from ..models.products import ProductCategory, Product
from .Mixins import LoginMixin
import os import os
import json import json
@@ -23,227 +20,95 @@ class TestProducts(TestCase):
self.assertIsNone(product.external_id) self.assertIsNone(product.external_id)
self.assertIsNone(product.unit_external_id) self.assertIsNone(product.unit_external_id)
def test_import_products(self):
self._import_csv()
all_products = self._get_products()
self.assertEqual(
len(all_products),
3
)
def test_import_products_with_categories(self):
self._import_csv()
all_products = self._get_products()
self.assertIn("Aceites", all_products[0]["categories"])
def test_don_repeat_categories_on_import(self):
self._import_csv()
categories_on_csv = ["Cafes", "Alimentos", "Aceites"]
categories = ProductCategory.objects.all()
self.assertCountEqual(
[c.name for c in categories],
categories_on_csv
)
def test_update_products(self): def test_update_products(self):
self._import_csv() self._import_csv()
first_products = self._get_products() first_products = self._get_products()
self._import_csv("example_products2.csv") self._import_csv('example_products2.csv')
seconds_products = self._get_products() seconds_products = self._get_products()
self.assertEqual(len(first_products), len(seconds_products)) self.assertEqual(len(first_products), len(seconds_products))
def test_preserve_id_on_import(self):
self._import_csv()
id_aceite = Product.objects.get(name='Aceite').id
self._import_csv('example_products2.csv')
id_post_updated = Product.objects.get(name='Aceite').id
self.assertEqual(id_aceite, id_post_updated)
def test_update_categories_on_import(self):
self._import_csv()
first_products = self._get_products()
first_categories = {p["name"]: p["categories"] for p in first_products}
self._import_csv('example_products2.csv')
updated_products = self._get_products()
updated_categories = {
p["name"]: p["categories"] for p in updated_products}
self.assertIn('Cafes', first_categories['Arroz'])
self.assertNotIn('Granos', first_categories['Arroz'])
self.assertIn('Granos', updated_categories['Arroz'])
self.assertNotIn('Cafes', updated_categories['Arroz'])
def test_update_price(self):
self._import_csv()
first_products = self._get_products()
first_prices = {p["name"]: p["price_list"] for p in first_products}
expected_first_prices = {
"Aceite": '50000.00',
"Café": '14000.00',
"Arroz": '7000.00'
}
self.assertDictEqual(
expected_first_prices,
first_prices
)
self._import_csv('example_products2.csv')
updated_products = self._get_products()
updated_prices = {p["name"]: p["price_list"] for p in updated_products}
expected_updated_prices = {
"Aceite": '50000.00',
"Café": '15000.00',
"Arroz": '6000.00'
}
self.assertDictEqual(
expected_updated_prices,
updated_prices
)
def _get_products(self): def _get_products(self):
products_response = self.client.get("/don_confiao/productos") products_response = self.client.get("/don_confiao/productos")
return json.loads(products_response.content.decode("utf-8")) return json.loads(products_response.content.decode('utf-8'))
def _import_csv(self, csv_file="example_products.csv"): def _import_csv(self, csv_file='example_products.csv'):
app_name = "don_confiao/tests/data_example" app_name = "don_confiao"
app_dir = os.path.join(settings.BASE_DIR, app_name) app_dir = os.path.join(settings.BASE_DIR, app_name)
example_csv = os.path.join(app_dir, csv_file) example_csv = os.path.join(app_dir, csv_file)
with open(example_csv, "rb") as csv: with open(example_csv, 'rb') as csv:
self.client.post("/don_confiao/importar_productos", {"csv_file": csv}) self.client.post(
"/don_confiao/importar_productos",
{"csv_file": csv}
class TestProductsAPIFiltering(APITestCase, LoginMixin): )
"""Tests for filtering products by active status via API"""
def setUp(self):
self.login()
# Create active products
self.active_product_1 = Product.objects.create(
name="Active Product 1", price=100.00, active=True
)
self.active_product_2 = Product.objects.create(
name="Active Product 2", price=200.00, active=True
)
# Create inactive products
self.inactive_product_1 = Product.objects.create(
name="Inactive Product 1", price=150.00, active=False
)
self.inactive_product_2 = Product.objects.create(
name="Inactive Product 2", price=250.00, active=False
)
def test_get_products_default_returns_only_active(self):
"""By default, API should return only active products"""
response = self.client.get("/don_confiao/api/products/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(len(data), 2)
product_names = [p["name"] for p in data]
self.assertIn("Active Product 1", product_names)
self.assertIn("Active Product 2", product_names)
self.assertNotIn("Inactive Product 1", product_names)
self.assertNotIn("Inactive Product 2", product_names)
def test_get_products_active_true(self):
"""Filter products with active=true should return only active products"""
response = self.client.get("/don_confiao/api/products/?active=true")
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(len(data), 2)
for product in data:
self.assertTrue(product["active"])
def test_get_products_active_false(self):
"""Filter products with active=false should return only inactive products"""
response = self.client.get("/don_confiao/api/products/?active=false")
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(len(data), 2)
for product in data:
self.assertFalse(product["active"])
product_names = [p["name"] for p in data]
self.assertIn("Inactive Product 1", product_names)
self.assertIn("Inactive Product 2", product_names)
def test_get_products_active_all(self):
"""Filter products with active=all should return all products"""
response = self.client.get("/don_confiao/api/products/?active=all")
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(len(data), 4)
def test_get_products_active_variations(self):
"""Test different variations of true/false values"""
# Test '1' for true
response = self.client.get("/don_confiao/api/products/?active=1")
self.assertEqual(len(response.json()), 2)
# Test 'yes' for true
response = self.client.get("/don_confiao/api/products/?active=yes")
self.assertEqual(len(response.json()), 2)
# Test '0' for false
response = self.client.get("/don_confiao/api/products/?active=0")
self.assertEqual(len(response.json()), 2)
# Test 'no' for false
response = self.client.get("/don_confiao/api/products/?active=no")
self.assertEqual(len(response.json()), 2)
def test_get_product_detail_regardless_of_status(self):
"""Getting a specific product by ID should work regardless of active status"""
# Get active product
response = self.client.get(
f"/don_confiao/api/products/{self.active_product_1.id}/"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["name"], "Active Product 1")
# Get inactive product
response = self.client.get(
f"/don_confiao/api/products/{self.inactive_product_1.id}/"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["name"], "Inactive Product 1")
class TestProductsAPIActivation(APITestCase, LoginMixin):
"""Tests for activating/deactivating products via API"""
def setUp(self):
self.login()
self.product = Product.objects.create(
name="Test Product", price=100.00, active=True
)
def test_deactivate_product_via_patch(self):
"""PATCH request should be able to deactivate a product"""
response = self.client.patch(
f"/don_confiao/api/products/{self.product.id}/",
{"active": False},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Verify product was deactivated
self.product.refresh_from_db()
self.assertFalse(self.product.active)
# Verify response contains updated data
self.assertFalse(response.json()["active"])
def test_activate_product_via_patch(self):
"""PATCH request should be able to activate a product"""
# First deactivate the product
self.product.active = False
self.product.save()
response = self.client.patch(
f"/don_confiao/api/products/{self.product.id}/",
{"active": True},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Verify product was activated
self.product.refresh_from_db()
self.assertTrue(self.product.active)
# Verify response contains updated data
self.assertTrue(response.json()["active"])
def test_update_other_fields_preserves_active_status(self):
"""Updating other fields should not affect active status"""
response = self.client.patch(
f"/don_confiao/api/products/{self.product.id}/",
{"price": "250.00"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Verify active status was preserved
self.product.refresh_from_db()
self.assertTrue(self.product.active)
self.assertEqual(self.product.price, 250.00)
def test_deactivated_product_not_in_default_list(self):
"""After deactivating a product, it should not appear in default list"""
# Deactivate product
self.client.patch(
f"/don_confiao/api/products/{self.product.id}/",
{"active": False},
format="json",
)
# Get default product list
response = self.client.get("/don_confiao/api/products/")
data = response.json()
# Product should not be in list
product_ids = [p["id"] for p in data]
self.assertNotIn(self.product.id, product_ids)
def test_activated_product_appears_in_default_list(self):
"""After activating a product, it should appear in default list"""
# Deactivate product first
self.product.active = False
self.product.save()
# Activate product
self.client.patch(
f"/don_confiao/api/products/{self.product.id}/",
{"active": True},
format="json",
)
# Get default product list
response = self.client.get("/don_confiao/api/products/")
data = response.json()
# Product should be in list
product_ids = [p["id"] for p in data]
self.assertIn(self.product.id, product_ids)

View File

@@ -3,7 +3,7 @@ from decimal import Decimal
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from ..models.products import Product from ..models import Product
from .Mixins import LoginMixin from .Mixins import LoginMixin
@@ -12,163 +12,120 @@ class TestProductsFromTryton(TestCase, LoginMixin):
self.login() self.login()
self.product = Product.objects.create( self.product = Product.objects.create(
name="Panela", name='Panela',
price=5000, price=5000,
measuring_unit="UNIT", measuring_unit='UNIT',
unit_external_id=1, unit_external_id=1,
external_id=191, external_id=191
) )
self.product.save() self.product.save()
self.product2 = Product.objects.create( self.product2 = Product.objects.create(
name="Papa", name='Papa',
price=4500, price=4500,
measuring_unit="Kilogram", measuring_unit='Kilogram',
unit_external_id=2, unit_external_id=2,
external_id=192, external_id=192
) )
self.product2.save() self.product2.save()
@patch("sabatron_tryton_rpc_client.client.Client.call") @patch('sabatron_tryton_rpc_client.client.Client.call')
@patch("sabatron_tryton_rpc_client.client.Client.connect") @patch('sabatron_tryton_rpc_client.client.Client.connect')
def test_create_import_products(self, mock_connect, mock_call): def test_create_import_products(self, mock_connect, mock_call):
mock_connect.return_value = None mock_connect.return_value = None
def fake_call(*args, **kwargs): def fake_call(*args, **kwargs):
product_search = "model.product.product.search" product_search = 'model.product.product.search'
search_args = [ search_args = [[["salable", "=", True]], 0, 1000, [['rec_name', 'ASC'], ['id', None]], {'company': 1}]
[["salable", "=", True]], if (args == (product_search, search_args)):
0,
1000,
[["rec_name", "ASC"], ["id", None]],
{"company": 1},
]
if args == (product_search, search_args):
return [190, 191, 192] return [190, 191, 192]
product_read = "model.product.product.read" product_read = 'model.product.product.read'
product_args = ( product_args = ([190, 191, 192],
[190, 191, 192], ['id', 'name', 'default_uom.id',
[ 'default_uom.rec_name', 'list_price'],
"id", {'company': 1}
"name", )
"default_uom.id", if (args == (product_read, product_args)):
"default_uom.rec_name",
"list_price",
],
{"company": 1},
)
if args == (product_read, product_args):
return [ return [
{ {'id': 190, 'list_price': Decimal('25000'),
"id": 190, 'name': 'Producto 1',
"list_price": Decimal("25000"), 'default_uom.': {'id': 1, 'rec_name': 'Unit'}},
"name": "Producto 1", {'id': 191, 'list_price': Decimal('6000'),
"default_uom.": {"id": 1, "rec_name": "Unit"}, 'name': 'Panela2',
}, 'default_uom.': {'id': 1, 'rec_name': 'Unit'}},
{ {'id': 192, 'list_price': Decimal('4500'),
"id": 191, 'name': 'Papa',
"list_price": Decimal("6000"), 'default_uom.': {'id': 2, 'rec_name': 'Kilogram'}},
"name": "Panela2",
"default_uom.": {"id": 1, "rec_name": "Unit"},
},
{
"id": 192,
"list_price": Decimal("4500"),
"name": "Papa",
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
},
] ]
raise Exception( raise Exception(f"Sorry, args non expected on this test: {args}")
f"Sorry, args non expected on this test: {args}"
)
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
url = "/don_confiao/api/importar_productos_de_tryton" url = '/don_confiao/api/importar_productos_de_tryton'
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
expected_response = { expected_response = {
"checked_tryton_products": [190, 191, 192], 'checked_tryton_products': [190, 191, 192],
"created_products": [3], 'created_products': [3],
"untouched_products": [2], 'untouched_products': [2],
"failed_products": [], 'failed_products': [],
"updated_products": [1], 'updated_products': [1]
} }
self.assertEqual(content, expected_response) self.assertEqual(content, expected_response)
created_product = Product.objects.get(id=3) created_product = Product.objects.get(id=3)
self.assertEqual(created_product.external_id, str(190)) self.assertEqual(created_product.external_id, str(190))
self.assertEqual(created_product.name, "Producto 1") self.assertEqual(created_product.name, 'Producto 1')
self.assertEqual(created_product.price, Decimal("25000")) self.assertEqual(created_product.price, Decimal('25000'))
self.assertEqual(created_product.measuring_unit, "Unit") self.assertEqual(created_product.measuring_unit, 'Unit')
updated_product = Product.objects.get(id=1) updated_product = Product.objects.get(id=1)
self.assertEqual(updated_product.external_id, str(191)) self.assertEqual(updated_product.external_id, str(191))
self.assertEqual(updated_product.name, "Panela2") self.assertEqual(updated_product.name, 'Panela2')
self.assertEqual(updated_product.price, Decimal("6000")) self.assertEqual(updated_product.price, Decimal('6000'))
self.assertEqual(updated_product.measuring_unit, "Unit") self.assertEqual(updated_product.measuring_unit, 'Unit')
@patch("sabatron_tryton_rpc_client.client.Client.call") @patch('sabatron_tryton_rpc_client.client.Client.call')
@patch("sabatron_tryton_rpc_client.client.Client.connect") @patch('sabatron_tryton_rpc_client.client.Client.connect')
def test_import_duplicated_name_products( def test_import_duplicated_name_products(self, mock_connect, mock_call):
self, mock_connect, mock_call
):
mock_connect.return_value = None mock_connect.return_value = None
def fake_call(*args, **kwargs): def fake_call(*args, **kwargs):
product_search = "model.product.product.search" product_search = 'model.product.product.search'
search_args = [ search_args = [[["salable", "=", True]], 0, 1000, [['rec_name', 'ASC'], ['id', None]], {'company': 1}]
[["salable", "=", True]], if (args == (product_search, search_args)):
0,
1000,
[["rec_name", "ASC"], ["id", None]],
{"company": 1},
]
if args == (product_search, search_args):
return [200] return [200]
product_read = "model.product.product.read" product_read = 'model.product.product.read'
product_args = ( product_args = ([200],
[200], ['id', 'name', 'default_uom.id',
[ 'default_uom.rec_name', 'list_price'],
"id", {'company': 1}
"name", )
"default_uom.id", if (args == (product_read, product_args)):
"default_uom.rec_name",
"list_price",
],
{"company": 1},
)
if args == (product_read, product_args):
return [ return [
{ {'id': 200, 'list_price': Decimal('25000'),
"id": 200, 'name': self.product.name,
"list_price": Decimal("25000"), 'default_uom.': {'id': 1, 'rec_name': 'Unit'}},
"name": self.product.name,
"default_uom.": {"id": 1, "rec_name": "Unit"},
},
] ]
raise Exception( raise Exception(f"Sorry, args non expected on this test: {args}")
f"Sorry, args non expected on this test: {args}"
)
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
url = "/don_confiao/api/importar_productos_de_tryton" url = '/don_confiao/api/importar_productos_de_tryton'
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode('utf-8'))
expected_response = { expected_response = {
"checked_tryton_products": [200], 'checked_tryton_products': [200],
"created_products": [], 'created_products': [],
"untouched_products": [], 'untouched_products': [],
"failed_products": [200], 'failed_products': [200],
"updated_products": [], 'updated_products': [],
} }
self.assertEqual(content, expected_response) self.assertEqual(content, expected_response)

View File

@@ -0,0 +1,51 @@
from django.test import Client, TestCase
from ..models import Payment, Sale, Product, Customer
class TestPurchaseWithPayment(TestCase):
def setUp(self):
self.client = Client()
self.product = Product()
self.product.name = "Arroz"
self.product.price = 5000
self.product.save()
customer = Customer()
customer.name = "Noelba Lopez"
customer.save()
self.customer = customer
def test_generate_payment_when_it_has_payment(self):
quantity = 2
unit_price = 2500
total = 5000
self.client.post(
'/don_confiao/comprar',
{
"customer": str(self.customer.id),
"date": "2024-07-27",
"phone": "3010101000",
"description": "Venta de contado",
"saleline_set-TOTAL_FORMS": "1",
"saleline_set-INITIAL_FORMS": "0",
"saleline_set-MIN_NUM_FORMS": "0",
"saleline_set-MAX_NUM_FORMS": "1000",
"saleline_set-0-product": str(self.product.id),
"saleline_set-0-quantity": str(quantity),
"saleline_set-0-unit_price": str(unit_price),
"saleline_set-0-description": "Linea de Venta",
"saleline_set-0-sale": "",
"saleline_set-0-id": "",
"quantity_lines": "1",
"quantity_products": str(quantity),
"ammount": str(quantity * unit_price),
"payment_method": "CASH",
}
)
purchases = Sale.objects.all()
self.assertEqual(1, len(purchases))
payments = Payment.objects.all()
self.assertEqual(1, len(payments))
self.assertEqual(total, payments[0].amount)
self.assertEqual('CASH', payments[0].type_payment)

View File

@@ -0,0 +1,55 @@
from django.test import TestCase
from ..models import Sale, Product, SaleLine, Customer
from .Mixins import LoginMixin
class TestSummaryViewPurchase(TestCase, LoginMixin):
def setUp(self):
self.login()
customer = Customer()
customer.name = 'Alejo Mono'
customer.save()
purchase = Sale()
purchase.customer = customer
purchase.date = "2024-07-30"
purchase.clean()
purchase.save()
product = Product()
product.name = "cafe"
product.price = "72500"
product.save()
line = SaleLine()
line.sale = purchase
line.product = product
line.quantity = "11"
line.unit_price = "72500"
line.save()
self.purchase = purchase
def test_summary_has_customer(self):
url = "/don_confiao/resumen_compra/" + str(self.purchase.id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.context["purchase"].customer,
self.purchase.customer
)
self.assertIn('Alejo Mono', response.content.decode('utf-8'))
def test_json_summary(self):
url = f"/don_confiao/resumen_compra_json/{self.purchase.id}"
response = self.client.get(url)
content = response.content.decode('utf-8')
self.assertEqual(response.status_code, 200)
self.assertIn('Alejo Mono', content)
self.assertIn('cafe', content)
self.assertIn('72500', content)
self.assertIn('quantity', content)
self.assertIn('11', content)
self.assertIn('date', content)
self.assertIn(self.purchase.date, content)
self.assertIn('lines', content)

View File

@@ -1,9 +1,7 @@
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ..models.customers import Customer from ..models import Customer, Product, Sale, SaleLine
from ..models.products import Product
from ..models.sales import Sale, SaleLine
class ConfiaoTest(TestCase): class ConfiaoTest(TestCase):
@@ -22,7 +20,7 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22 12:05:00" sale.date = "2024-06-22 12:05:00"
sale.phone = "666666666" sale.phone = '666666666'
sale.description = "Description" sale.description = "Description"
sale.save() sale.save()
@@ -32,9 +30,9 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22 12:05:00" sale.date = "2024-06-22 12:05:00"
sale.phone = "666666666" sale.phone = '666666666'
sale.description = "Description" sale.description = "Description"
sale.payment_method = "" sale.payment_method = ''
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
sale.full_clean() sale.full_clean()
@@ -43,7 +41,7 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22"
sale.phone = "666666666" sale.phone = '666666666'
sale.description = "Description" sale.description = "Description"
line = SaleLine() line = SaleLine()
@@ -60,7 +58,7 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22"
sale.phone = "666666666" sale.phone = '666666666'
sale.description = "Description" sale.description = "Description"
line1 = SaleLine() line1 = SaleLine()
@@ -83,14 +81,15 @@ class ConfiaoTest(TestCase):
self.assertEqual(len(SaleLine.objects.all()), 2) self.assertEqual(len(SaleLine.objects.all()), 2)
self.assertEqual( self.assertEqual(
Sale.objects.all()[0].saleline_set.all()[0].quantity, 2 Sale.objects.all()[0].saleline_set.all()[0].quantity,
2
) )
def test_allow_sale_without_description(self): def test_allow_sale_without_description(self):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22"
sale.phone = "666666666" sale.phone = '666666666'
sale.description = None sale.description = None
sale.save() sale.save()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
from django.test import TestCase
from ..forms import PurchaseForm
from ..models import Customer
_csrf_token = \
"bVjBevJRavxRPFOlVgAWiyh9ceuiwPlyEcmbPZprNuCGHjFZRKZrBeunJvKTRgOx"
class PurchaseFormTest(TestCase):
def setUp(self):
self.customer = Customer()
self.customer.name = "Don Confiao Gonzalez"
self.customer.address = "Patio Bonito"
self.customer.save()
def test_add_purchase(self):
form_data = {
"csrfmiddlewaretoken": _csrf_token,
"customer": self.customer.id,
"date": "2024-08-03",
"payment_method": "CASH",
"phone": "sfasfd",
"description": "dasdadad",
"saleline_set-TOTAL_FORMS": "1",
"saleline_set-INITIAL_FORMS": "0",
"saleline_set-MIN_NUM_FORMS": "0",
"saleline_set-MAX_NUM_FORMS": "1000",
"saleline_set-0-product": "5",
"saleline_set-0-quantity": "1",
"saleline_set-0-unit_price": "500",
"saleline_set-0-description": "afasdfasdf",
"saleline_set-0-sale": "",
"saleline_set-0-id": "",
"quantity_lines": "1",
"quantity_products": "1",
"ammount": "500",
"form": ""
}
purchase_form = PurchaseForm(data=form_data)
purchase_form.is_valid()
# raise Exception(purchase_form)
self.assertTrue(purchase_form.is_valid())

View File

@@ -4,58 +4,39 @@ from rest_framework.routers import DefaultRouter
from . import views from . import views
from . import api_views from . import api_views
app_name = "don_confiao" app_name = 'don_confiao'
router = DefaultRouter() router = DefaultRouter()
router.register(r"sales", api_views.SaleView, basename="sale") router.register(r'sales', api_views.SaleView, basename='sale')
router.register( router.register(r'customers', api_views.CustomerView, basename='customer')
r"catalog_sales", api_views.CatalogSaleView, basename="catalog_sale" router.register(r'products', api_views.ProductView, basename='product')
) router.register(r'reconciliate_jar', api_views.ReconciliateJarModelView,
router.register(r"customers", api_views.CustomerView, basename="customer") basename='reconciliate_jar')
router.register(r"products", api_views.ProductView, basename="product")
router.register(
r"reconciliate_jar",
api_views.ReconciliateJarModelView,
basename="reconciliate_jar",
)
urlpatterns = [ urlpatterns = [
path("", views.index, name="wellcome"),
path("comprar", views.buy, name="buy"),
path("compras", views.purchases, name="purchases"),
path("productos", views.products, name="products"), path("productos", views.products, name="products"),
path( path("lista_productos", views.ProductListView.as_view(), name='product_list'),
"resumen_compra_json/<int:id>", path("importar_productos", views.import_products, name="import_products"),
api_views.SaleSummary.as_view(), path('api/importar_productos_de_tryton',
name="purchase_json_summary", api_views.ProductsFromTrytonView.as_view(),
), name="products_from_tryton"),
path( path("importar_terceros", views.import_customers, name="import_customers"),
"payment_methods/all/select_format", path('api/importar_clientes_de_tryton',
api_views.PaymentMethodView.as_view(), api_views.CustomersFromTrytonView.as_view(),
name="payment_methods_to_select", name="customers_from_tryton"),
), path("exportar_ventas_para_tryton",
path( views.exportar_ventas_para_tryton,
"purchases/for_reconciliation", name="exportar_ventas_para_tryton"),
api_views.SalesForReconciliationView.as_view(), path('api/enviar_ventas_a_tryton', api_views.SalesToTrytonView.as_view(), name="send_tryton"),
name="sales_for_reconciliation", path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"),
), path("resumen_compra_json/<int:id>", api_views.SaleSummary.as_view(), name="purchase_json_summary"),
path("reconciliate_jar", api_views.ReconciliateJarView.as_view()), path("payment_methods/all/select_format", api_views.PaymentMethodView.as_view(), name="payment_methods_to_select"),
path("api/", include(router.urls)), path('purchases/for_reconciliation', api_views.SalesForReconciliationView.as_view(), name='sales_for_reconciliation'),
path( path('reconciliate_jar', api_views.ReconciliateJarView.as_view()),
"api/importar_productos_de_tryton", path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()),
api_views.ProductsFromTrytonView.as_view(), path('api/sales/for_tryton', api_views.SalesForTrytonView.as_view()),
name="products_from_tryton", path('api/', include(router.urls)),
),
path(
"api/importar_clientes_de_tryton",
api_views.CustomersFromTrytonView.as_view(),
name="customers_from_tryton",
),
path(
"api/enviar_ventas_a_tryton",
api_views.SalesToTrytonView.as_view(),
name="send_tryton",
),
path(
"api/admin_code/validate/<code>",
api_views.AdminCodeValidateView.as_view(),
),
path("api/sales/for_tryton", api_views.SalesForTrytonView.as_view()),
] ]

View File

@@ -3,11 +3,14 @@ from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.views.generic import ListView from django.views.generic import ListView
from django.db import transaction from django.db import transaction
from .models.sales import Sale, SaleLine from .models import (
from .models.customers import Customer Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar)
from .models.sales import Sale, SaleLine, Payment from .forms import (
from .models.products import Product, ProductCategory ImportProductsForm,
from .models.payments import PaymentMethods, ReconciliationJar ImportCustomersForm,
PurchaseForm,
SaleLineFormSet,
PurchaseSummaryForm)
import csv import csv
import io import io
@@ -23,13 +26,105 @@ class DecimalEncoder(json.JSONEncoder):
def index(request): def index(request):
return render(request, "don_confiao/index.html") return render(request, 'don_confiao/index.html')
def buy(request):
if request.method == "POST":
sale_form = PurchaseForm(request.POST)
line_formset = SaleLineFormSet(request.POST)
sale_summary_form = PurchaseSummaryForm(request.POST)
forms_are_valid = all([
sale_form.is_valid(),
line_formset.is_valid(),
sale_summary_form.is_valid()
])
payment_method = request.POST.get('payment_method')
valid_payment_methods = [PaymentMethods.CASH]
valid_payment_method = payment_method in valid_payment_methods
if forms_are_valid:
with transaction.atomic():
sale = sale_form.save()
line_formset.instance = sale
line_formset.save()
Payment.total_payment_from_sale(
payment_method,
sale
)
return HttpResponseRedirect("compras")
else:
sale_form = PurchaseForm()
line_formset = SaleLineFormSet()
sale_summary_form = PurchaseSummaryForm()
return render(
request,
'don_confiao/purchase.html',
{
'sale_form': sale_form,
'linea_formset': line_formset,
'summary_form': sale_summary_form,
'list_products': json.dumps(Product.to_list(), cls=DecimalEncoder),
}
)
def purchases(request):
purchases = Sale.objects.all()
context = {
"purchases": purchases,
}
return render(request, 'don_confiao/purchases.html', context)
def products(request): def products(request):
return JsonResponse(Product.to_list(), safe=False) return JsonResponse(Product.to_list(), safe=False)
def import_products(request):
if request.method == "POST":
form = ImportProductsForm(request.POST, request.FILES)
if form.is_valid():
handle_import_products_file(request.FILES["csv_file"])
return HttpResponseRedirect("productos")
else:
form = ImportProductsForm()
return render(
request,
"don_confiao/import_products.html",
{'form': form}
)
def import_customers(request):
if request.method == "POST":
form = ImportCustomersForm(request.POST, request.FILES)
if form.is_valid():
handle_import_customers_file(request.FILES["csv_file"])
return HttpResponseRedirect("productos")
else:
form = ImportCustomersForm()
return render(
request,
"don_confiao/import_customers.html",
{'form': form}
)
def reconciliations(request):
return HttpResponse('<h1>Reconciliaciones</h1>')
def purchase_summary(request, id):
purchase = Sale.objects.get(pk=id)
return render(
request,
"don_confiao/purchase_summary.html",
{
"purchase": purchase
}
)
def _categories_from_csv_string(categories_string, separator="&"): def _categories_from_csv_string(categories_string, separator="&"):
categories = categories_string.split(separator) categories = categories_string.split(separator)
clean_categories = [c.strip() for c in categories] clean_categories = [c.strip() for c in categories]
@@ -41,32 +136,33 @@ def _category_from_name(name):
def handle_import_products_file(csv_file): def handle_import_products_file(csv_file):
data = io.StringIO(csv_file.read().decode("utf-8")) data = io.StringIO(csv_file.read().decode('utf-8'))
reader = csv.DictReader(data, quotechar='"') reader = csv.DictReader(data, quotechar='"')
for row in reader: for row in reader:
product, created = Product.objects.update_or_create( product, created = Product.objects.update_or_create(
name=row["producto"], name=row['producto'],
defaults={ defaults={
"price": row["precio"], 'price': row['precio'],
"measuring_unit": row["unidad"], 'measuring_unit': row['unidad']
}, }
) )
categories = _categories_from_csv_string(row["categorias"]) categories = _categories_from_csv_string(row["categorias"])
product.categories.clear() product.categories.clear()
for category in categories: for category in categories:
product.categories.add(category) product.categories.add(category)
def handle_import_customers_file(csv_file): def handle_import_customers_file(csv_file):
data = io.StringIO(csv_file.read().decode("utf-8")) data = io.StringIO(csv_file.read().decode('utf-8'))
reader = csv.DictReader(data, quotechar='"') reader = csv.DictReader(data, quotechar='"')
for row in reader: for row in reader:
customer, created = Customer.objects.update_or_create( customer, created = Customer.objects.update_or_create(
name=row["nombre"], name=row['nombre'],
defaults={"email": row["correo"], "phone": row["telefono"]}, defaults={
'email': row['correo'],
'phone': row['telefono']
}
) )
def sales_to_tryton_csv(sales): def sales_to_tryton_csv(sales):
tryton_sales_header = [ tryton_sales_header = [
"Tercero", "Tercero",
@@ -86,7 +182,7 @@ def sales_to_tryton_csv(sales):
"Tienda", "Tienda",
"Terminal de venta", "Terminal de venta",
"Autorecogida", "Autorecogida",
"Comentario", "Comentario"
] ]
csv_data = [tryton_sales_header] csv_data = [tryton_sales_header]
@@ -98,7 +194,7 @@ def sales_to_tryton_csv(sales):
first_sale_line = sale_lines[0] first_sale_line = sale_lines[0]
customer_info = [sale.customer.name] * 3 + [sale.description] * 2 customer_info = [sale.customer.name] * 3 + [sale.description] * 2
first_line = customer_info + [ first_line = customer_info + [
sale.date.strftime("%Y-%m-%d"), sale.date.strftime('%Y-%m-%d'),
"Contado", "Contado",
"Almacén", "Almacén",
"Peso colombiano", "Peso colombiano",
@@ -110,21 +206,39 @@ def sales_to_tryton_csv(sales):
"Tienda La Ilusion", "Tienda La Ilusion",
"La Ilusion", "La Ilusion",
True, True,
sale.description, sale.description]
]
lines.append(first_line) lines.append(first_line)
for line in sale_lines[1:]: for line in sale_lines[1:]:
lines.append( lines.append([""]*9+[
[""] * 9 line.product.name,
+ [ line.quantity,
line.product.name, line.unit_price,
line.quantity, "Unidad"]+[""]*5)
line.unit_price,
"Unidad",
]
+ [""] * 5
)
for row in lines: for row in lines:
csv_data.append(row) csv_data.append(row)
return csv_data return csv_data
def exportar_ventas_para_tryton(request):
if request.method == "GET":
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = "attachment; filename=sales.csv"
sales = Sale.objects.all()
csv_data = sales_to_tryton_csv(sales)
writer = csv.writer(response)
for row in csv_data:
writer.writerow(row)
return response
class ProductListView(ListView):
model = Product
template_model = 'don_confiao/product_list.html'
def get_queryset(self):
name = self.request.GET.get('name')
if name:
return Product.objects.filter(name__icontains=name)
return Product.objects.all()

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tienda_ilusion.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@@ -19,5 +18,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == '__main__':
main() main()

View File

@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tienda_ilusion.settings')
application = get_asgi_application() application = get_asgi_application()

View File

@@ -0,0 +1,157 @@
"""
Django settings for tienda_ilusion project.
Generated by 'django-admin startproject' using Django 5.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
"SECRET_KEY",
"django-insecure-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v"
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True if os.environ.get("DEBUG", 'False') in ['True', '1'] else False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')
CORS_ALLOWED_ORIGINS = os.environ.get(
'CORS_ALLOWED_ORIGINS',
'http://localhost:3000,http://localhost:7001').split(',')
# Application definition
INSTALLED_APPS = [
'don_confiao.apps.DonConfiaoConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'users',
# 'don_confiao'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
]
ROOT_URLCONF = 'tienda_ilusion.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'tienda_ilusion/templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'tienda_ilusion.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
FIXTURE_DIRS = ['don_confiao/tests/Fixtures']
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"AUTH_HEADER_TYPES": ("Bearer",),
}
# CORS_ALLOWED_ORIGINS = [
# "http://localhost:5173",
# ]

View File

@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tienda_ilusion.settings')
application = get_wsgi_application() application = get_wsgi_application()