Compare commits
2 Commits
2415ed3564
...
feature/37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
079f4e3806 | ||
|
|
6473a8348c |
17
.dockerignore
Normal file
17
.dockerignore
Normal 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/
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
4
.env_example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
TRYTON_HOST=localhost
|
||||||
|
TRYTON_DATABASE=tryton
|
||||||
|
TRYTON_USERNAME=admin
|
||||||
|
TRYTON_PASSWORD=admin
|
||||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -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
377
AGENTS.md
@@ -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
269
README.md
@@ -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
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
# Refactorización a Domain-Driven Design - Resumen
|
|
||||||
|
|
||||||
## ✅ Refactorización Completada
|
|
||||||
|
|
||||||
**Fecha**: 29 de Mayo 2026
|
|
||||||
**Commit**: `47e87e42048e2394a6461f78d5c8e6a4386aa2f9`
|
|
||||||
**Tests**: 46 tests pasando ✓
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Estadísticas
|
|
||||||
|
|
||||||
- **Archivos creados**: 17
|
|
||||||
- **Archivos eliminados**: 2
|
|
||||||
- **Archivos modificados**: 1
|
|
||||||
- **Líneas agregadas**: +816
|
|
||||||
- **Líneas eliminadas**: -621
|
|
||||||
- **Balance neto**: +195 líneas (mejor organización, más documentación)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Nueva Estructura
|
|
||||||
|
|
||||||
```
|
|
||||||
don_confiao/
|
|
||||||
├── models/ # ✅ Ya estaba organizado por dominio
|
|
||||||
│ ├── products.py
|
|
||||||
│ ├── customers.py
|
|
||||||
│ ├── sales.py
|
|
||||||
│ ├── payments.py
|
|
||||||
│ └── admin.py
|
|
||||||
│
|
|
||||||
├── serializers/ # 🆕 Nuevo - Organizado por dominio
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── products.py # ProductSerializer, ListProductSerializer
|
|
||||||
│ ├── customers.py # CustomerSerializer, ListCustomerSerializer
|
|
||||||
│ ├── sales.py # Sale, SaleLine, CatalogSale serializers
|
|
||||||
│ └── payments.py # ReconciliationJar, PaymentMethod serializers
|
|
||||||
│
|
|
||||||
├── api/ # 🆕 Nuevo - API Views por dominio
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── products.py # ProductView, ProductsFromTrytonView
|
|
||||||
│ ├── customers.py # CustomerView, CustomersFromTrytonView
|
|
||||||
│ ├── sales.py # SaleView, CatalogSaleView, Sales*TrytonView
|
|
||||||
│ ├── payments.py # ReconciliateJarView, PaymentMethodView
|
|
||||||
│ └── admin.py # AdminCodeValidateView
|
|
||||||
│
|
|
||||||
└── services/ # 🆕 Nuevo - Capa de servicios
|
|
||||||
└── tryton/
|
|
||||||
├── __init__.py
|
|
||||||
├── client.py # Factory + TrytonSale/LineSale
|
|
||||||
├── products.py # ProductTrytonService
|
|
||||||
├── customers.py # CustomerTrytonService
|
|
||||||
└── sales.py # SaleTrytonService
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Detalles por Dominio
|
|
||||||
|
|
||||||
### 1. Products (Productos)
|
|
||||||
|
|
||||||
**Serializers** (`serializers/products.py`):
|
|
||||||
- `ProductSerializer` - Serializer completo de producto
|
|
||||||
- `ListProductSerializer` - Versión simplificada para listados
|
|
||||||
|
|
||||||
**API Views** (`api/products.py`):
|
|
||||||
- `ProductView` - CRUD de productos con filtrado por `active` status
|
|
||||||
- `ProductsFromTrytonView` - Importación desde Tryton
|
|
||||||
|
|
||||||
**Services** (`services/tryton/products.py`):
|
|
||||||
- `ProductTrytonService` - Lógica de sincronización con Tryton
|
|
||||||
- `import_from_tryton()` - Importa productos
|
|
||||||
- `_create_product()` - Crea nuevo producto
|
|
||||||
- `_update_product()` - Actualiza producto existente
|
|
||||||
- `_need_update()` - Determina si necesita actualización
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Customers (Clientes)
|
|
||||||
|
|
||||||
**Serializers** (`serializers/customers.py`):
|
|
||||||
- `CustomerSerializer` - Serializer completo de cliente
|
|
||||||
- `ListCustomerSerializer` - Versión simplificada para listados
|
|
||||||
|
|
||||||
**API Views** (`api/customers.py`):
|
|
||||||
- `CustomerView` - CRUD de clientes
|
|
||||||
- `CustomersFromTrytonView` - Importación desde Tryton
|
|
||||||
|
|
||||||
**Services** (`services/tryton/customers.py`):
|
|
||||||
- `CustomerTrytonService` - Lógica de sincronización con Tryton
|
|
||||||
- `import_from_tryton()` - Importa clientes
|
|
||||||
- `_create_customer()` - Crea nuevo cliente
|
|
||||||
- `_update_customer()` - Actualiza cliente existente
|
|
||||||
- `_need_update()` - Determina si necesita actualización
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Sales (Ventas)
|
|
||||||
|
|
||||||
**Serializers** (`serializers/sales.py`):
|
|
||||||
- `SaleSerializer` - Serializer de venta
|
|
||||||
- `SaleLineSerializer` - Serializer de línea de venta
|
|
||||||
- `CatalogSaleSerializer` - Serializer de venta por catálogo
|
|
||||||
- `CatalogSaleLineSerializer` - Línea de venta por catálogo
|
|
||||||
- `SummarySaleLineSerializer` - Resumen de línea (con detalles)
|
|
||||||
- `SaleSummarySerializer` - Resumen completo de venta
|
|
||||||
- `SaleForRenconciliationSerializer` - Ventas para reconciliación
|
|
||||||
|
|
||||||
**API Views** (`api/sales.py`):
|
|
||||||
- `SaleView` - CRUD de ventas
|
|
||||||
- `CatalogSaleView` - CRUD de ventas por catálogo
|
|
||||||
- `SaleSummary` - Resumen de venta por ID
|
|
||||||
- `SalesForTrytonView` - Exportar ventas a CSV para Tryton
|
|
||||||
- `SalesToTrytonView` - Enviar ventas a Tryton
|
|
||||||
|
|
||||||
**Services** (`services/tryton/sales.py`):
|
|
||||||
- `SaleTrytonService` - Lógica de sincronización con Tryton
|
|
||||||
- `send_to_tryton()` - Envía ventas a Tryton
|
|
||||||
- `_to_tryton_params()` - Convierte venta a formato Tryton
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Payments (Pagos y Reconciliación)
|
|
||||||
|
|
||||||
**Serializers** (`serializers/payments.py`):
|
|
||||||
- `ReconciliationJarSerializer` - Serializer de arqueo de caja
|
|
||||||
- `PaymentMethodSerializer` - Serializer de métodos de pago
|
|
||||||
|
|
||||||
**API Views** (`api/payments.py`):
|
|
||||||
- `ReconciliateJarView` - Vista de reconciliación (POST/GET)
|
|
||||||
- `ReconciliateJarModelView` - ViewSet para arqueos
|
|
||||||
- `PaymentMethodView` - Listado de métodos de pago
|
|
||||||
- `SalesForReconciliationView` - Ventas pendientes de reconciliación
|
|
||||||
- `Pagination` - Paginación personalizada
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Admin
|
|
||||||
|
|
||||||
**API Views** (`api/admin.py`):
|
|
||||||
- `AdminCodeValidateView` - Validación de códigos de administrador
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Services - Tryton Integration
|
|
||||||
|
|
||||||
**Client** (`services/tryton/client.py`):
|
|
||||||
- `get_tryton_client()` - Factory para crear cliente conectado
|
|
||||||
- `TrytonSale` - Representa venta para exportación
|
|
||||||
- `TrytonLineSale` - Representa línea de venta para exportación
|
|
||||||
- Constantes de configuración (HOST, DATABASE, CURRENCY, etc.)
|
|
||||||
|
|
||||||
**Características**:
|
|
||||||
- ✅ Configuración centralizada
|
|
||||||
- ✅ Factory pattern para cliente
|
|
||||||
- ✅ DTOs para transformación de datos
|
|
||||||
- ✅ Reutilizable desde cualquier vista
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Migración de Código
|
|
||||||
|
|
||||||
### Archivos Eliminados
|
|
||||||
|
|
||||||
❌ `serializers.py` (170 líneas) → ✅ `serializers/` (4 archivos)
|
|
||||||
❌ `api_views.py` (526 líneas) → ✅ `api/` (5 archivos)
|
|
||||||
|
|
||||||
### Backwards Compatibility
|
|
||||||
|
|
||||||
Todas las importaciones existentes siguen funcionando gracias a `__init__.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Antes y Después - Mismas importaciones funcionan
|
|
||||||
from don_confiao.serializers import ProductSerializer
|
|
||||||
from don_confiao.api import ProductView
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Beneficios Obtenidos
|
|
||||||
|
|
||||||
### 1. Cohesión
|
|
||||||
- Cada módulo agrupa código relacionado a un solo dominio
|
|
||||||
- Fácil encontrar todo lo relacionado a un concepto (ej: productos)
|
|
||||||
|
|
||||||
### 2. Separación de Responsabilidades
|
|
||||||
- **Models**: Definición de datos
|
|
||||||
- **Serializers**: Transformación API ↔ Modelos
|
|
||||||
- **API Views**: Lógica de endpoints
|
|
||||||
- **Services**: Lógica de negocio (Tryton)
|
|
||||||
|
|
||||||
### 3. Mantenibilidad
|
|
||||||
- Archivos más pequeños y enfocados
|
|
||||||
- Menos de 150 líneas por archivo
|
|
||||||
- Más fácil de leer y entender
|
|
||||||
|
|
||||||
### 4. Escalabilidad
|
|
||||||
- Agregar nuevo dominio = crear nuevos archivos en cada capa
|
|
||||||
- No modifica código existente
|
|
||||||
- Open/Closed Principle
|
|
||||||
|
|
||||||
### 5. Testabilidad
|
|
||||||
- Tests pueden organizarse por dominio
|
|
||||||
- Servicios fáciles de mockear
|
|
||||||
- Cada componente testeable independientemente
|
|
||||||
|
|
||||||
### 6. Reutilización
|
|
||||||
- Servicios Tryton usables desde cualquier vista
|
|
||||||
- Serializers compartibles entre vistas
|
|
||||||
- DRY (Don't Repeat Yourself)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Próximos Pasos Sugeridos
|
|
||||||
|
|
||||||
### 1. Organizar Tests por Dominio (Opcional)
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/
|
|
||||||
├── __init__.py
|
|
||||||
├── products/
|
|
||||||
│ ├── test_products_api.py
|
|
||||||
│ ├── test_products_serializers.py
|
|
||||||
│ └── test_products_tryton.py
|
|
||||||
├── customers/
|
|
||||||
│ └── ...
|
|
||||||
└── sales/
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Documentación
|
|
||||||
|
|
||||||
Crear documentación por dominio:
|
|
||||||
```
|
|
||||||
docs/
|
|
||||||
├── products.md
|
|
||||||
├── customers.md
|
|
||||||
├── sales.md
|
|
||||||
└── tryton_integration.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Mejorar Services Layer
|
|
||||||
|
|
||||||
Agregar más lógica de negocio a services:
|
|
||||||
- Validaciones complejas
|
|
||||||
- Cálculos de negocio
|
|
||||||
- Transformaciones de datos
|
|
||||||
|
|
||||||
### 4. Agregar Type Hints
|
|
||||||
|
|
||||||
Mejorar type hints en services para mejor IDE support:
|
|
||||||
```python
|
|
||||||
def import_from_tryton(self) -> dict[str, list[int]]:
|
|
||||||
"""Importa productos desde Tryton
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict con listas de IDs: checked, failed, updated, created, untouched
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests
|
|
||||||
|
|
||||||
### Estado Actual
|
|
||||||
✅ **46 tests pasando**
|
|
||||||
|
|
||||||
### Cobertura por Dominio
|
|
||||||
- ✅ Products: 13 tests (incluye filtrado por active status)
|
|
||||||
- ✅ Sales: 8 tests
|
|
||||||
- ✅ Customers: Tests de integración Tryton
|
|
||||||
- ✅ Payments: Tests de reconciliación
|
|
||||||
- ✅ Admin: Tests de códigos
|
|
||||||
|
|
||||||
### Ejecución
|
|
||||||
```bash
|
|
||||||
# Todos los tests
|
|
||||||
docker compose -f docker-compose.dev.yml run --rm django python manage.py test don_confiao.tests
|
|
||||||
|
|
||||||
# Tests específicos
|
|
||||||
docker compose -f docker-compose.dev.yml run --rm django python manage.py test don_confiao.tests.test_products
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Ejemplos de Uso
|
|
||||||
|
|
||||||
### Importar Serializers
|
|
||||||
```python
|
|
||||||
# Forma explícita
|
|
||||||
from don_confiao.serializers.products import ProductSerializer
|
|
||||||
|
|
||||||
# Forma simplificada (recomendada)
|
|
||||||
from don_confiao.serializers import ProductSerializer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Importar API Views
|
|
||||||
```python
|
|
||||||
# Forma explícita
|
|
||||||
from don_confiao.api.products import ProductView
|
|
||||||
|
|
||||||
# Forma simplificada (recomendada)
|
|
||||||
from don_confiao.api import ProductView
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usar Services
|
|
||||||
```python
|
|
||||||
from don_confiao.services import get_tryton_client, ProductTrytonService
|
|
||||||
|
|
||||||
# En una vista
|
|
||||||
def my_view(request):
|
|
||||||
client = get_tryton_client()
|
|
||||||
service = ProductTrytonService(client)
|
|
||||||
result = service.import_from_tryton()
|
|
||||||
return Response(result)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Verificación de Calidad
|
|
||||||
|
|
||||||
### Checks Realizados
|
|
||||||
- ✅ Todos los tests pasan
|
|
||||||
- ✅ Sin imports circulares
|
|
||||||
- ✅ Todas las URLs funcionando
|
|
||||||
- ✅ Backwards compatibility mantenida
|
|
||||||
- ✅ Código limpio y documentado
|
|
||||||
|
|
||||||
### Métricas
|
|
||||||
- **Complejidad ciclomática**: Reducida (archivos más pequeños)
|
|
||||||
- **Acoplamiento**: Reducido (separación de capas)
|
|
||||||
- **Cohesión**: Incrementada (código por dominio)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Principios Aplicados
|
|
||||||
|
|
||||||
1. **Domain-Driven Design (DDD)**: Organización por dominios de negocio
|
|
||||||
2. **Single Responsibility Principle (SRP)**: Cada módulo una responsabilidad
|
|
||||||
3. **Open/Closed Principle**: Abierto a extensión, cerrado a modificación
|
|
||||||
4. **Dependency Inversion**: Dependencias hacia abstracciones (services)
|
|
||||||
5. **DRY**: Servicios reutilizables, no repetir lógica
|
|
||||||
6. **Separation of Concerns**: Capas bien definidas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Soporte
|
|
||||||
|
|
||||||
Si tienes preguntas sobre la nueva estructura:
|
|
||||||
|
|
||||||
1. **Serializers**: Ver `serializers/__init__.py` para exportaciones
|
|
||||||
2. **API Views**: Ver `api/__init__.py` para exportaciones
|
|
||||||
3. **Services**: Ver `services/tryton/` para lógica Tryton
|
|
||||||
4. **URLs**: Ver `urls.py` para rutas disponibles
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**¡Refactorización completada exitosamente! 🎉**
|
|
||||||
|
|
||||||
La estructura está lista para escalar y mantener de forma eficiente.
|
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
11
docker-compose.override.yml
Normal file
11
docker-compose.override.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
django:
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: django.Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./tienda_ilusion:/app/
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "7000:9090"
|
||||||
@@ -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
|
|
||||||
@@ -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
8
docker-compose.yml
Normal 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
288
poetry.lock
generated
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -3,16 +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
|
|
||||||
Pillow # Image processing for catalogue image resizing
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -1,192 +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"
|
|
||||||
|
|
||||||
# Media files
|
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
|
||||||
MEDIA_URL = "/media/"
|
|
||||||
|
|
||||||
# Catalogue image settings
|
|
||||||
CATALOGUE_IMAGE_WIDTH = int(os.environ.get("CATALOGUE_IMAGE_WIDTH", "600"))
|
|
||||||
CATALOGUE_IMAGE_HEIGHT = int(os.environ.get("CATALOGUE_IMAGE_HEIGHT", "600"))
|
|
||||||
CATALOGUE_STRICT_DIMENSION = os.environ.get("CATALOGUE_STRICT_DIMENSION", "False").lower() in ("true", "1", "yes")
|
|
||||||
CATALOGUE_BACKGROUND_IMAGES_RGBA = os.environ.get(
|
|
||||||
"CATALOGUE_BACKGROUND_IMAGES_RGBA", "0,0,0,0"
|
|
||||||
)
|
|
||||||
# Parse RGBA string to tuple
|
|
||||||
_rgba_parts = CATALOGUE_BACKGROUND_IMAGES_RGBA.split(",")
|
|
||||||
CATALOGUE_BACKGROUND_IMAGES_RGBA = tuple(int(p.strip()) for p in _rgba_parts)
|
|
||||||
CATALOGUE_MAX_UPLOAD_SIZE = int(os.environ.get("CATALOGUE_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024)))
|
|
||||||
|
|
||||||
# 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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,119 +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/"
|
|
||||||
|
|
||||||
# Media files configuration
|
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
|
||||||
MEDIA_URL = "/media/"
|
|
||||||
|
|
||||||
# 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',
|
|
||||||
# ]
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,33 +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.register(Customer)
|
|
||||||
class CustomerAdmin(admin.ModelAdmin):
|
|
||||||
list_display = (
|
|
||||||
"name",
|
|
||||||
"email",
|
|
||||||
"phone",
|
|
||||||
"external_id",
|
|
||||||
"address_external_id",
|
|
||||||
)
|
|
||||||
search_fields = ("name", "email", "phone")
|
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
from .catalogue_images import CatalogueImageViewSet
|
|
||||||
from .products import ProductView, ProductsFromTrytonView
|
|
||||||
from .customers import CustomerView, CustomersFromTrytonView
|
|
||||||
from .sales import (
|
|
||||||
SaleView,
|
|
||||||
CatalogSaleView,
|
|
||||||
SaleSummary,
|
|
||||||
CatalogSaleSummary,
|
|
||||||
SalesForTrytonView,
|
|
||||||
SalesToTrytonView,
|
|
||||||
CatalogSalesToTrytonView,
|
|
||||||
)
|
|
||||||
from .payments import (
|
|
||||||
ReconciliateJarView,
|
|
||||||
ReconciliateJarModelView,
|
|
||||||
PaymentMethodView,
|
|
||||||
SalesForReconciliationView,
|
|
||||||
Pagination,
|
|
||||||
)
|
|
||||||
from .admin import AdminCodeValidateView
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# Catalogue Images
|
|
||||||
"CatalogueImageViewSet",
|
|
||||||
# Products
|
|
||||||
"ProductView",
|
|
||||||
"ProductsFromTrytonView",
|
|
||||||
# Customers
|
|
||||||
"CustomerView",
|
|
||||||
"CustomersFromTrytonView",
|
|
||||||
# Sales
|
|
||||||
"SaleView",
|
|
||||||
"CatalogSaleView",
|
|
||||||
"SaleSummary",
|
|
||||||
"CatalogSaleSummary",
|
|
||||||
"SalesForTrytonView",
|
|
||||||
"SalesToTrytonView",
|
|
||||||
"CatalogSalesToTrytonView",
|
|
||||||
# Payments
|
|
||||||
"ReconciliateJarView",
|
|
||||||
"ReconciliateJarModelView",
|
|
||||||
"PaymentMethodView",
|
|
||||||
"SalesForReconciliationView",
|
|
||||||
"Pagination",
|
|
||||||
# Admin
|
|
||||||
"AdminCodeValidateView",
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
from ..models.admin import AdminCode
|
|
||||||
from ..permissions import IsAdministrator
|
|
||||||
|
|
||||||
|
|
||||||
class AdminCodeValidateView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def get(self, request, code):
|
|
||||||
codes = AdminCode.objects.filter(value=code)
|
|
||||||
return Response({"validCode": bool(codes)})
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
from ..image_service import resize_catalogue_image
|
|
||||||
from ..models.catalogue_images import CatalogueImage
|
|
||||||
from ..permissions import IsAdministrator
|
|
||||||
from ..serializers.catalogue_images import CatalogueImageSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogueImageViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = CatalogueImage.objects.all()
|
|
||||||
serializer_class = CatalogueImageSerializer
|
|
||||||
|
|
||||||
def get_permissions(self):
|
|
||||||
if self.action in ("create", "update", "partial_update", "destroy"):
|
|
||||||
return [IsAuthenticated(), IsAdministrator()]
|
|
||||||
return [IsAuthenticated()]
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
instance = serializer.save()
|
|
||||||
resize_catalogue_image(instance.image)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
instance = serializer.save()
|
|
||||||
resize_catalogue_image(instance.image)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
from ..models.customers import Customer
|
|
||||||
from ..serializers import CustomerSerializer
|
|
||||||
from ..permissions import IsAdministrator
|
|
||||||
from ..services.tryton.customers import CustomerTrytonService
|
|
||||||
from ..services.tryton.client import get_tryton_client
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerView(viewsets.ModelViewSet):
|
|
||||||
queryset = Customer.objects.all()
|
|
||||||
serializer_class = CustomerSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class CustomersFromTrytonView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
tryton_client = get_tryton_client()
|
|
||||||
service = CustomerTrytonService(tryton_client)
|
|
||||||
result = service.import_from_tryton()
|
|
||||||
return Response(result, status=200)
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
|
||||||
from rest_framework.pagination import PageNumberPagination
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from ..models.sales import Sale
|
|
||||||
from ..models.payments import ReconciliationJar, PaymentMethods
|
|
||||||
from ..serializers import (
|
|
||||||
ReconciliationJarSerializer,
|
|
||||||
PaymentMethodSerializer,
|
|
||||||
SaleForRenconciliationSerializer,
|
|
||||||
)
|
|
||||||
from ..permissions import IsAdministrator
|
|
||||||
|
|
||||||
|
|
||||||
class Pagination(PageNumberPagination):
|
|
||||||
page_size = 10
|
|
||||||
page_size_query_param = "page_size"
|
|
||||||
|
|
||||||
|
|
||||||
class ReconciliateJarView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
data = request.data
|
|
||||||
cash_purchases_id = data.get("cash_purchases")
|
|
||||||
serializer = ReconciliationJarSerializer(data=data)
|
|
||||||
if serializer.is_valid():
|
|
||||||
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
|
|
||||||
if not self._is_valid_total(
|
|
||||||
cash_purchases, data.get("total_cash_purchases")
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "total_cash_purchases not equal to sum of all purchases."
|
|
||||||
},
|
|
||||||
status=HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
reconciliation = serializer.save()
|
|
||||||
other_purchases = self._get_other_purchases(data.get("other_totals"))
|
|
||||||
|
|
||||||
self._link_purchases(reconciliation, cash_purchases, other_purchases)
|
|
||||||
return Response({"id": reconciliation.id})
|
|
||||||
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
reconciliations = ReconciliationJar.objects.all()
|
|
||||||
serializer = ReconciliationJarSerializer(reconciliations, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
def _is_valid_total(self, purchases, total):
|
|
||||||
calculated_total = sum(p.get_total() for p in purchases)
|
|
||||||
return Decimal(calculated_total).quantize(Decimal(".0001")) == (
|
|
||||||
Decimal(total).quantize(Decimal(".0001"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_other_purchases(self, other_totals):
|
|
||||||
if not other_totals:
|
|
||||||
return []
|
|
||||||
purchases = []
|
|
||||||
for method in other_totals:
|
|
||||||
purchases.extend(other_totals[method]["purchases"])
|
|
||||||
if purchases:
|
|
||||||
return Sale.objects.filter(pk__in=purchases)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _link_purchases(self, reconciliation, cash_purchases, other_purchases):
|
|
||||||
for purchase in cash_purchases:
|
|
||||||
purchase.reconciliation = reconciliation
|
|
||||||
purchase.clean()
|
|
||||||
purchase.save()
|
|
||||||
|
|
||||||
for purchase in other_purchases:
|
|
||||||
purchase.reconciliation = reconciliation
|
|
||||||
purchase.clean()
|
|
||||||
purchase.save()
|
|
||||||
|
|
||||||
|
|
||||||
class ReconciliateJarModelView(viewsets.ModelViewSet):
|
|
||||||
queryset = ReconciliationJar.objects.all().order_by("-date_time")
|
|
||||||
pagination_class = Pagination
|
|
||||||
serializer_class = ReconciliationJarSerializer
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class SalesForReconciliationView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
sales = Sale.objects.filter(reconciliation=None)
|
|
||||||
grouped_sales = {}
|
|
||||||
|
|
||||||
for sale in sales:
|
|
||||||
if sale.payment_method not in grouped_sales.keys():
|
|
||||||
grouped_sales[sale.payment_method] = []
|
|
||||||
serializer = SaleForRenconciliationSerializer(sale)
|
|
||||||
grouped_sales[sale.payment_method].append(serializer.data)
|
|
||||||
|
|
||||||
return Response(grouped_sales)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
from ..models.products import Product
|
|
||||||
from ..serializers import ProductSerializer
|
|
||||||
from ..permissions import IsAdministrator
|
|
||||||
from ..services.tryton.products import ProductTrytonService
|
|
||||||
from ..services.tryton.client import get_tryton_client
|
|
||||||
|
|
||||||
|
|
||||||
class ProductView(viewsets.ModelViewSet):
|
|
||||||
queryset = Product.objects.all()
|
|
||||||
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 ProductsFromTrytonView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
tryton_client = get_tryton_client()
|
|
||||||
service = ProductTrytonService(tryton_client)
|
|
||||||
result = service.import_from_tryton()
|
|
||||||
return Response(result, status=200)
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
import io
|
|
||||||
import csv
|
|
||||||
|
|
||||||
from ..models.sales import Sale, SaleLine, CatalogSale
|
|
||||||
from ..models.customers import Customer
|
|
||||||
from ..models.products import Product
|
|
||||||
from ..serializers import (
|
|
||||||
SaleSerializer,
|
|
||||||
CatalogSaleSerializer,
|
|
||||||
SaleSummarySerializer,
|
|
||||||
CatalogSaleSummarySerializer,
|
|
||||||
)
|
|
||||||
from ..permissions import IsAdministrator
|
|
||||||
from ..services.tryton.sales import SaleTrytonService
|
|
||||||
from ..services.tryton.client import get_tryton_client
|
|
||||||
from ..views import sales_to_tryton_csv
|
|
||||||
|
|
||||||
|
|
||||||
class SaleView(viewsets.ModelViewSet):
|
|
||||||
queryset = Sale.objects.all()
|
|
||||||
serializer_class = SaleSerializer
|
|
||||||
|
|
||||||
def create(self, request):
|
|
||||||
data = request.data
|
|
||||||
customer = Customer.objects.get(pk=data["customer"])
|
|
||||||
date = data["date"]
|
|
||||||
lines = data["saleline_set"]
|
|
||||||
payment_method = data["payment_method"]
|
|
||||||
description = data.get("notes", "")
|
|
||||||
sale = Sale.objects.create(
|
|
||||||
customer=customer,
|
|
||||||
date=date,
|
|
||||||
payment_method=payment_method,
|
|
||||||
description=description,
|
|
||||||
)
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
product = Product.objects.get(pk=line["product"])
|
|
||||||
quantity = line["quantity"]
|
|
||||||
unit_price = line["unit_price"]
|
|
||||||
SaleLine.objects.create(
|
|
||||||
sale=sale,
|
|
||||||
product=product,
|
|
||||||
quantity=quantity,
|
|
||||||
unit_price=unit_price,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{"id": sale.id, "message": "Venta creada con exito"},
|
|
||||||
status=201,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogSaleView(viewsets.ModelViewSet):
|
|
||||||
queryset = CatalogSale.objects.all()
|
|
||||||
serializer_class = CatalogSaleSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class SaleSummary(APIView):
|
|
||||||
def get(self, request, id):
|
|
||||||
sale = Sale.objects.get(pk=id)
|
|
||||||
serializer = SaleSummarySerializer(sale)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogSaleSummary(APIView):
|
|
||||||
def get(self, request, id):
|
|
||||||
catalog_sale = CatalogSale.objects.get(pk=id)
|
|
||||||
serializer = CatalogSaleSummarySerializer(catalog_sale)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class SalesForTrytonView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
sales = Sale.objects.all()
|
|
||||||
csv_data = self._generate_sales_CSV(sales)
|
|
||||||
return Response({"csv": csv_data})
|
|
||||||
|
|
||||||
def _generate_sales_CSV(self, sales):
|
|
||||||
output = io.StringIO()
|
|
||||||
writer = csv.writer(output)
|
|
||||||
csv_data = sales_to_tryton_csv(sales)
|
|
||||||
|
|
||||||
for row in csv_data:
|
|
||||||
writer.writerow(row)
|
|
||||||
return output.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
class SalesToTrytonView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
tryton_client = get_tryton_client()
|
|
||||||
service = SaleTrytonService(tryton_client)
|
|
||||||
result = service.send_to_tryton()
|
|
||||||
return Response(result, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogSalesToTrytonView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
tryton_client = get_tryton_client()
|
|
||||||
service = SaleTrytonService(tryton_client)
|
|
||||||
result = service.send_catalog_sales_to_tryton()
|
|
||||||
return Response(result, status=200)
|
|
||||||
457
tienda_ilusion/don_confiao/api_views.py
Normal file
457
tienda_ilusion/don_confiao/api_views.py
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
|
||||||
|
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
|
||||||
|
from .views import sales_to_tryton_csv
|
||||||
|
from .permissions import IsAdministrator
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from sabatron_tryton_rpc_client.client import Client
|
||||||
|
import io
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
|
||||||
|
TRYTON_HOST = os.environ.get('TRYTON_HOST', 'localhost')
|
||||||
|
TRYTON_DATABASE = os.environ.get('TRYTON_DATABASE', 'tryton')
|
||||||
|
TRYTON_USERNAME = os.environ.get('TRYTON_USERNAME', 'admin')
|
||||||
|
TRYTON_PASSWORD = os.environ.get('TRYTON_PASSWORD', 'admin')
|
||||||
|
TRYTON_COP_CURRENCY = 31
|
||||||
|
TRYTON_COMPANY_ID = 1
|
||||||
|
TRYTON_SHOPS = [1]
|
||||||
|
|
||||||
|
|
||||||
|
class Pagination(PageNumberPagination):
|
||||||
|
page_size = 10
|
||||||
|
page_size_query_param = 'page_size'
|
||||||
|
|
||||||
|
|
||||||
|
class SaleView(viewsets.ModelViewSet):
|
||||||
|
queryset = Sale.objects.all()
|
||||||
|
serializer_class = SaleSerializer
|
||||||
|
|
||||||
|
def create(self, request):
|
||||||
|
data = request.data
|
||||||
|
customer = Customer.objects.get(pk=data['customer'])
|
||||||
|
date = data['date']
|
||||||
|
lines = data['saleline_set']
|
||||||
|
payment_method = data['payment_method']
|
||||||
|
description = data.get('notes', '')
|
||||||
|
sale = Sale.objects.create(
|
||||||
|
customer=customer,
|
||||||
|
date=date,
|
||||||
|
payment_method=payment_method,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
product = Product.objects.get(pk=line['product'])
|
||||||
|
quantity = line['quantity']
|
||||||
|
unit_price = line['unit_price']
|
||||||
|
SaleLine.objects.create(
|
||||||
|
sale=sale,
|
||||||
|
product=product,
|
||||||
|
quantity=quantity,
|
||||||
|
unit_price=unit_price
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{'id': sale.id, 'message': 'Venta creada con exito'},
|
||||||
|
status=201
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductView(viewsets.ModelViewSet):
|
||||||
|
queryset = Product.objects.all()
|
||||||
|
serializer_class = ProductSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerView(viewsets.ModelViewSet):
|
||||||
|
queryset = Customer.objects.all()
|
||||||
|
serializer_class = CustomerSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ReconciliateJarView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
data = request.data
|
||||||
|
cash_purchases_id = data.get('cash_purchases')
|
||||||
|
serializer = ReconciliationJarSerializer(data=data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
|
||||||
|
if not self._is_valid_total(cash_purchases, data.get('total_cash_purchases')):
|
||||||
|
return Response(
|
||||||
|
{'error': 'total_cash_purchases not equal to sum of all purchases.'},
|
||||||
|
status=HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
reconciliation = serializer.save()
|
||||||
|
other_purchases = self._get_other_purchases(data.get('other_totals'))
|
||||||
|
|
||||||
|
self._link_purchases(reconciliation, cash_purchases, other_purchases)
|
||||||
|
return Response({'id': reconciliation.id})
|
||||||
|
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
reconciliations = ReconciliationJar.objects.all()
|
||||||
|
serializer = ReconciliationJarSerializer(reconciliations, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def _is_valid_total(self, purchases, total):
|
||||||
|
calculated_total = sum(p.get_total() for p in purchases)
|
||||||
|
return Decimal(calculated_total).quantize(Decimal('.0001')) == (
|
||||||
|
Decimal(total).quantize(Decimal('.0001')))
|
||||||
|
|
||||||
|
def _get_other_purchases(self, other_totals):
|
||||||
|
if not other_totals:
|
||||||
|
return []
|
||||||
|
purchases = []
|
||||||
|
for method in other_totals:
|
||||||
|
purchases.extend(other_totals[method]['purchases'])
|
||||||
|
if purchases:
|
||||||
|
return Sale.objects.filter(pk__in=purchases)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _link_purchases(self, reconciliation, cash_purchases, other_purchases):
|
||||||
|
for purchase in cash_purchases:
|
||||||
|
purchase.reconciliation = reconciliation
|
||||||
|
purchase.clean()
|
||||||
|
purchase.save()
|
||||||
|
|
||||||
|
for purchase in other_purchases:
|
||||||
|
purchase.reconciliation = reconciliation
|
||||||
|
purchase.clean()
|
||||||
|
purchase.save()
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodView(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class SalesForReconciliationView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
sales = Sale.objects.filter(reconciliation=None)
|
||||||
|
grouped_sales = {}
|
||||||
|
|
||||||
|
for sale in sales:
|
||||||
|
if sale.payment_method not in grouped_sales.keys():
|
||||||
|
grouped_sales[sale.payment_method] = []
|
||||||
|
serializer = SaleForRenconciliationSerializer(sale)
|
||||||
|
grouped_sales[sale.payment_method].append(serializer.data)
|
||||||
|
|
||||||
|
return Response(grouped_sales)
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSummary(APIView):
|
||||||
|
def get(self, request, id):
|
||||||
|
sale = Sale.objects.get(pk=id)
|
||||||
|
serializer = SaleSummarySerializer(sale)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminCodeValidateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def get(self, request, code):
|
||||||
|
codes = AdminCode.objects.filter(value=code)
|
||||||
|
return Response({'validCode': bool(codes)})
|
||||||
|
|
||||||
|
|
||||||
|
class ReconciliateJarModelView(viewsets.ModelViewSet):
|
||||||
|
queryset = ReconciliationJar.objects.all().order_by('-date_time')
|
||||||
|
pagination_class = Pagination
|
||||||
|
serializer_class = ReconciliationJarSerializer
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
|
||||||
|
class SalesForTrytonView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
sales = Sale.objects.all()
|
||||||
|
csv = self._generate_sales_CSV(sales)
|
||||||
|
return Response({'csv': csv})
|
||||||
|
|
||||||
|
def _generate_sales_CSV(self, sales):
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
csv_data = sales_to_tryton_csv(sales)
|
||||||
|
|
||||||
|
for row in csv_data:
|
||||||
|
writer.writerow(row)
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class SalesToTrytonView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
tryton_client = Client(
|
||||||
|
hostname=TRYTON_HOST,
|
||||||
|
database=TRYTON_DATABASE,
|
||||||
|
username=TRYTON_USERNAME,
|
||||||
|
password=TRYTON_PASSWORD
|
||||||
|
)
|
||||||
|
tryton_client.connect()
|
||||||
|
method = 'model.sale.sale.create'
|
||||||
|
tryton_context = {'company': TRYTON_COMPANY_ID,
|
||||||
|
'shops': TRYTON_SHOPS}
|
||||||
|
|
||||||
|
successful = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
sales = Sale.objects.filter(external_id=None)
|
||||||
|
for sale in sales:
|
||||||
|
try:
|
||||||
|
lines = SaleLine.objects.filter(sale=sale.id)
|
||||||
|
tryton_params = self.__to_tryton_params(sale, lines, tryton_context)
|
||||||
|
external_ids = tryton_client.call(method, tryton_params)
|
||||||
|
sale.external_id = external_ids[0]
|
||||||
|
sale.save()
|
||||||
|
successful.append(sale.id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al enviar la venta: {e}"
|
||||||
|
f"venta_id: {sale.id}")
|
||||||
|
failed.append(sale.id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{'successful': successful, 'failed': failed},
|
||||||
|
status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
def __to_tryton_params(self, sale, lines, tryton_context):
|
||||||
|
sale_tryton = TrytonSale(sale, lines)
|
||||||
|
return [[sale_tryton.to_tryton()], tryton_context]
|
||||||
|
|
||||||
|
|
||||||
|
class TrytonSale:
|
||||||
|
|
||||||
|
def __init__(self, sale, lines):
|
||||||
|
self.sale = sale
|
||||||
|
self.lines = lines
|
||||||
|
|
||||||
|
def _format_date(self, _date):
|
||||||
|
return {"__class__": "date", "year": _date.year, "month": _date.month,
|
||||||
|
"day": _date.day}
|
||||||
|
|
||||||
|
def to_tryton(self):
|
||||||
|
return {
|
||||||
|
"company": TRYTON_COMPANY_ID,
|
||||||
|
"shipment_address": self.sale.customer.address_external_id,
|
||||||
|
"invoice_address": self.sale.customer.address_external_id,
|
||||||
|
"currency": TRYTON_COP_CURRENCY,
|
||||||
|
"comment": self.sale.description or '',
|
||||||
|
"description": "Metodo pago: " + str(
|
||||||
|
self.sale.payment_method or ''
|
||||||
|
),
|
||||||
|
"party": self.sale.customer.external_id,
|
||||||
|
"reference": "don_confiao " + str(self.sale.id),
|
||||||
|
"sale_date": self._format_date(self.sale.date),
|
||||||
|
"lines": [[
|
||||||
|
"create",
|
||||||
|
[TrytonLineSale(line).to_tryton() for line in self.lines]
|
||||||
|
]],
|
||||||
|
"self_pick_up": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TrytonLineSale:
|
||||||
|
def __init__(self, sale_line):
|
||||||
|
self.sale_line = sale_line
|
||||||
|
|
||||||
|
def _format_decimal(self, number):
|
||||||
|
return {"__class__": "Decimal", "decimal": str(number)}
|
||||||
|
|
||||||
|
def to_tryton(self):
|
||||||
|
return {
|
||||||
|
"product": self.sale_line.product.external_id,
|
||||||
|
"quantity": self._format_decimal(self.sale_line.quantity),
|
||||||
|
"type": "line",
|
||||||
|
"unit": self.sale_line.product.unit_external_id,
|
||||||
|
"unit_price": self._format_decimal(self.sale_line.unit_price)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProductsFromTrytonView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
tryton_client = Client(
|
||||||
|
hostname=TRYTON_HOST,
|
||||||
|
database=TRYTON_DATABASE,
|
||||||
|
username=TRYTON_USERNAME,
|
||||||
|
password=TRYTON_PASSWORD
|
||||||
|
)
|
||||||
|
tryton_client.connect()
|
||||||
|
method = 'model.product.product.search'
|
||||||
|
context = {'company': 1}
|
||||||
|
params = [[["salable", "=", True]], 0, 1000, [["rec_name", "ASC"], ["id", None]], context]
|
||||||
|
product_ids = tryton_client.call(method, params)
|
||||||
|
tryton_products = self.__get_product_datails_from_tryton(
|
||||||
|
product_ids, tryton_client, context
|
||||||
|
)
|
||||||
|
checked_tryton_products = product_ids
|
||||||
|
failed_products = []
|
||||||
|
updated_products = []
|
||||||
|
created_products = []
|
||||||
|
untouched_products = []
|
||||||
|
|
||||||
|
for tryton_product in tryton_products:
|
||||||
|
try:
|
||||||
|
product = Product.objects.get(
|
||||||
|
external_id=tryton_product.get('id')
|
||||||
|
)
|
||||||
|
except Product.DoesNotExist:
|
||||||
|
try:
|
||||||
|
product = self.__create_product(tryton_product)
|
||||||
|
created_products.append(product.id)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al importar productos: {e}"
|
||||||
|
f"El producto: {tryton_product}")
|
||||||
|
failed_products.append(tryton_product.get('id'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.__need_update(product, tryton_product):
|
||||||
|
self.__update_product(product, tryton_product)
|
||||||
|
updated_products.append(product.id)
|
||||||
|
else:
|
||||||
|
untouched_products.append(product.id)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'checked_tryton_products': checked_tryton_products,
|
||||||
|
'failed_products': failed_products,
|
||||||
|
'updated_products': updated_products,
|
||||||
|
'created_products': created_products,
|
||||||
|
'untouched_products': untouched_products,
|
||||||
|
},
|
||||||
|
status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get_product_datails_from_tryton(self, product_ids, tryton_client, context):
|
||||||
|
tryton_fields = ['id', 'name', 'default_uom.id',
|
||||||
|
'default_uom.rec_name', 'list_price']
|
||||||
|
method = 'model.product.product.read'
|
||||||
|
params = (product_ids, tryton_fields, context)
|
||||||
|
response = tryton_client.call(method, params)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def __need_update(self, product, tryton_product):
|
||||||
|
if not product.name == tryton_product.get('name'):
|
||||||
|
return True
|
||||||
|
if not product.price == tryton_product.get('list_price'):
|
||||||
|
return True
|
||||||
|
unit = tryton_product.get('default_uom.')
|
||||||
|
if not product.measuring_unit == unit.get('rec_name'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __create_product(self, tryton_product):
|
||||||
|
product = Product()
|
||||||
|
product.name = tryton_product.get('name')
|
||||||
|
product.price = tryton_product.get('list_price')
|
||||||
|
product.external_id = tryton_product.get('id')
|
||||||
|
unit = tryton_product.get('default_uom.')
|
||||||
|
product.measuring_unit = unit.get('rec_name')
|
||||||
|
product.unit_external_id = unit.get('id')
|
||||||
|
product.save()
|
||||||
|
return product
|
||||||
|
|
||||||
|
def __update_product(self, product, tryton_product):
|
||||||
|
product.name = tryton_product.get('name')
|
||||||
|
product.price = tryton_product.get('list_price')
|
||||||
|
product.external_id = tryton_product.get('id')
|
||||||
|
unit = tryton_product.get('default_uom.')
|
||||||
|
product.measuring_unit = unit.get('rec_name')
|
||||||
|
product.unit_external_id = unit.get('id')
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomersFromTrytonView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
tryton_client = Client(
|
||||||
|
hostname=TRYTON_HOST,
|
||||||
|
database=TRYTON_DATABASE,
|
||||||
|
username=TRYTON_USERNAME,
|
||||||
|
password=TRYTON_PASSWORD
|
||||||
|
)
|
||||||
|
tryton_client.connect()
|
||||||
|
method = 'model.party.party.search'
|
||||||
|
context = {'company': 1}
|
||||||
|
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
|
||||||
|
party_ids = tryton_client.call(method, params)
|
||||||
|
tryton_parties = self.__get_party_datails(
|
||||||
|
party_ids, tryton_client, context
|
||||||
|
)
|
||||||
|
checked_tryton_parties = party_ids
|
||||||
|
failed_parties = []
|
||||||
|
updated_customers = []
|
||||||
|
created_customers = []
|
||||||
|
untouched_customers = []
|
||||||
|
|
||||||
|
for tryton_party in tryton_parties:
|
||||||
|
try:
|
||||||
|
customer = Customer.objects.get(
|
||||||
|
external_id=tryton_party.get('id')
|
||||||
|
)
|
||||||
|
except Customer.DoesNotExist:
|
||||||
|
customer = self.__create_customer(tryton_party)
|
||||||
|
created_customers.append(customer.id)
|
||||||
|
continue
|
||||||
|
if self.__need_update(customer, tryton_party):
|
||||||
|
self.__update_customer(customer, tryton_party)
|
||||||
|
updated_customers.append(customer.id)
|
||||||
|
else:
|
||||||
|
untouched_customers.append(customer.id)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'checked_tryton_parties': checked_tryton_parties,
|
||||||
|
'failed_parties': failed_parties,
|
||||||
|
'updated_customers': updated_customers,
|
||||||
|
'created_customers': created_customers,
|
||||||
|
'untouched_customers': untouched_customers,
|
||||||
|
},
|
||||||
|
status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get_party_datails(self, party_ids, tryton_client, context):
|
||||||
|
tryton_fields = ['id', 'name', 'addresses']
|
||||||
|
method = 'model.party.party.read'
|
||||||
|
params = (party_ids, tryton_fields, context)
|
||||||
|
response = tryton_client.call(method, params)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def __need_update(self, customer, tryton_party):
|
||||||
|
if not customer.name == tryton_party.get('name'):
|
||||||
|
return True
|
||||||
|
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
|
||||||
|
if not customer.address_external_id == str(tryton_party.get('addresses')[0]):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __create_customer(self, tryton_party):
|
||||||
|
customer = Customer()
|
||||||
|
customer.name = tryton_party.get('name')
|
||||||
|
customer.external_id = tryton_party.get('id')
|
||||||
|
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
|
||||||
|
customer.address_external_id = tryton_party.get('addresses')[0]
|
||||||
|
customer.save()
|
||||||
|
return customer
|
||||||
|
|
||||||
|
def __update_customer(self, customer, tryton_party):
|
||||||
|
customer.name = tryton_party.get('name')
|
||||||
|
customer.external_id = tryton_party.get('id')
|
||||||
|
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
|
||||||
|
customer.address_external_id = tryton_party.get('addresses')[0]
|
||||||
|
customer.save()
|
||||||
1
tienda_ilusion/don_confiao/export_csv.py
Normal file
1
tienda_ilusion/don_confiao/export_csv.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
66
tienda_ilusion/don_confiao/forms.py
Normal file
66
tienda_ilusion/don_confiao/forms.py
Normal 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__'
|
||||||
|
)
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
|
|
||||||
def resize_catalogue_image(image_field):
|
|
||||||
width = settings.CATALOGUE_IMAGE_WIDTH
|
|
||||||
height = settings.CATALOGUE_IMAGE_HEIGHT
|
|
||||||
strict = settings.CATALOGUE_STRICT_DIMENSION
|
|
||||||
bg_color = settings.CATALOGUE_BACKGROUND_IMAGES_RGBA
|
|
||||||
|
|
||||||
img = Image.open(image_field.path)
|
|
||||||
img = img.convert("RGBA")
|
|
||||||
|
|
||||||
if strict:
|
|
||||||
img = resize_to_fit(img, width, height)
|
|
||||||
canvas = Image.new("RGBA", (width, height), bg_color)
|
|
||||||
x = (width - img.width) // 2
|
|
||||||
y = (height - img.height) // 2
|
|
||||||
canvas.paste(img, (x, y), img)
|
|
||||||
img = canvas
|
|
||||||
else:
|
|
||||||
if img.width > width:
|
|
||||||
new_height = int(img.height * width / img.width)
|
|
||||||
img = img.resize((width, new_height), Image.LANCZOS)
|
|
||||||
|
|
||||||
img.save(image_field.path, format="PNG")
|
|
||||||
|
|
||||||
|
|
||||||
def resize_to_fit(img, max_width, max_height):
|
|
||||||
ratio = min(max_width / img.width, max_height / img.height)
|
|
||||||
new_width = int(img.width * ratio)
|
|
||||||
new_height = int(img.height * ratio)
|
|
||||||
return img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.6 on 2026-05-31 01:46
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('don_confiao', '0046_product_active'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='catalogsale',
|
|
||||||
name='external_id',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 5.0.6 on 2026-06-05 14:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('don_confiao', '0047_catalogsale_external_id'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='catalogsale',
|
|
||||||
name='customer_address',
|
|
||||||
field=models.CharField(blank=True, max_length=255, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='catalogsale',
|
|
||||||
name='customer_name',
|
|
||||||
field=models.CharField(blank=True, max_length=255, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='catalogsale',
|
|
||||||
name='customer_phone',
|
|
||||||
field=models.CharField(blank=True, max_length=13, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='catalogsale',
|
|
||||||
name='pickup_method',
|
|
||||||
field=models.CharField(blank=True, max_length=30, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.0.6 on 2026-06-13 18:31
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import don_confiao.models.catalogue_images
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('don_confiao', '0048_catalogsale_customer_address_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CatalogueImage',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('image', models.ImageField(upload_to=don_confiao.models.catalogue_images._catalogue_image_upload_path)),
|
|
||||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='catalogue_images', to='don_confiao.product')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
210
tienda_ilusion/don_confiao/models.py
Normal file
210
tienda_ilusion/don_confiao/models.py
Normal 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)
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class AdminCode(models.Model):
|
|
||||||
value = models.CharField(max_length=255, null=False, blank=False)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
from .products import Product
|
|
||||||
|
|
||||||
|
|
||||||
def _catalogue_image_upload_path(instance, filename):
|
|
||||||
return f"catalogue_images/{instance.product_id}/{filename}"
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogueImage(models.Model):
|
|
||||||
product = models.ForeignKey(
|
|
||||||
Product,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="catalogue_images",
|
|
||||||
)
|
|
||||||
image = models.ImageField(upload_to=_catalogue_image_upload_path)
|
|
||||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"CatalogueImage for {self.product.name} ({self.id})"
|
|
||||||
@@ -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
|
|
||||||
@@ -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.")}
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
@@ -1,143 +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):
|
|
||||||
external_id = models.CharField(max_length=100, null=True, blank=True)
|
|
||||||
customer_name = models.CharField(max_length=255, null=True, blank=True)
|
|
||||||
customer_phone = models.CharField(max_length=13, null=True, blank=True)
|
|
||||||
customer_address = models.CharField(
|
|
||||||
max_length=255, null=True, blank=True
|
|
||||||
)
|
|
||||||
pickup_method = models.CharField(max_length=30, null=True, blank=True)
|
|
||||||
|
|
||||||
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)
|
|
||||||
103
tienda_ilusion/don_confiao/serializers.py
Normal file
103
tienda_ilusion/don_confiao/serializers.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Sale, SaleLine, Product, Customer, ReconciliationJar
|
||||||
|
|
||||||
|
|
||||||
|
class SaleLineSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SaleLine
|
||||||
|
fields = ['id', 'sale', 'product', 'unit_price', 'quantity']
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSerializer(serializers.ModelSerializer):
|
||||||
|
total = serializers.ReadOnlyField(source='get_total')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Sale
|
||||||
|
fields = ['id', 'customer', 'date', 'saleline_set',
|
||||||
|
'total', 'payment_method', 'external_id']
|
||||||
|
|
||||||
|
|
||||||
|
class ProductSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = ['id', 'name', 'price', 'measuring_unit', 'categories']
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Customer
|
||||||
|
fields = ['id', 'name', 'address', 'email', 'phone', 'external_id']
|
||||||
|
|
||||||
|
|
||||||
|
class ReconciliationJarSerializer(serializers.ModelSerializer):
|
||||||
|
Sales = SaleSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ReconciliationJar
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'date_time',
|
||||||
|
'reconcilier',
|
||||||
|
'cash_taken',
|
||||||
|
'cash_discrepancy',
|
||||||
|
'total_cash_purchases',
|
||||||
|
'Sales',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodSerializer(serializers.Serializer):
|
||||||
|
text = serializers.CharField()
|
||||||
|
value = serializers.CharField()
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
return {
|
||||||
|
'text': instance[1],
|
||||||
|
'value': instance[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SaleForRenconciliationSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField()
|
||||||
|
date = serializers.DateTimeField()
|
||||||
|
payment_method = serializers.CharField()
|
||||||
|
customer = serializers.SerializerMethodField()
|
||||||
|
total = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_customer(self, sale):
|
||||||
|
return {
|
||||||
|
'id': sale.customer.id,
|
||||||
|
'name': sale.customer.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_total(self, sale):
|
||||||
|
return sale.get_total()
|
||||||
|
|
||||||
|
|
||||||
|
class ListCustomerSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Customer
|
||||||
|
fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class ListProductSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class SummarySaleLineSerializer(serializers.ModelSerializer):
|
||||||
|
product = ListProductSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SaleLine
|
||||||
|
fields = ['product', 'quantity', 'unit_price', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSummarySerializer(serializers.ModelSerializer):
|
||||||
|
customer = ListCustomerSerializer()
|
||||||
|
lines = SummarySaleLineSerializer(many=True, source='saleline_set')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Sale
|
||||||
|
fields = ['id', 'date', 'customer', 'payment_method', 'lines']
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from .catalogue_images import CatalogueImageSerializer
|
|
||||||
from .products import ProductSerializer, ListProductSerializer
|
|
||||||
from .customers import CustomerSerializer, ListCustomerSerializer
|
|
||||||
from .sales import (
|
|
||||||
SaleSerializer,
|
|
||||||
SaleLineSerializer,
|
|
||||||
CatalogSaleSerializer,
|
|
||||||
CatalogSaleLineSerializer,
|
|
||||||
SummarySaleLineSerializer,
|
|
||||||
SaleSummarySerializer,
|
|
||||||
CatalogSummarySaleLineSerializer,
|
|
||||||
CatalogSaleSummarySerializer,
|
|
||||||
SaleForRenconciliationSerializer,
|
|
||||||
)
|
|
||||||
from .payments import (
|
|
||||||
ReconciliationJarSerializer,
|
|
||||||
PaymentMethodSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# Catalogue Images
|
|
||||||
"CatalogueImageSerializer",
|
|
||||||
# Products
|
|
||||||
"ProductSerializer",
|
|
||||||
"ListProductSerializer",
|
|
||||||
# Customers
|
|
||||||
"CustomerSerializer",
|
|
||||||
"ListCustomerSerializer",
|
|
||||||
# Sales
|
|
||||||
"SaleSerializer",
|
|
||||||
"SaleLineSerializer",
|
|
||||||
"CatalogSaleSerializer",
|
|
||||||
"CatalogSaleLineSerializer",
|
|
||||||
"SummarySaleLineSerializer",
|
|
||||||
"SaleSummarySerializer",
|
|
||||||
"CatalogSummarySaleLineSerializer",
|
|
||||||
"CatalogSaleSummarySerializer",
|
|
||||||
"SaleForRenconciliationSerializer",
|
|
||||||
# Payments
|
|
||||||
"ReconciliationJarSerializer",
|
|
||||||
"PaymentMethodSerializer",
|
|
||||||
]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ..models.catalogue_images import CatalogueImage
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogueImageSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = CatalogueImage
|
|
||||||
fields = ["id", "product", "image", "uploaded_at"]
|
|
||||||
read_only_fields = ["uploaded_at"]
|
|
||||||
|
|
||||||
def validate_image(self, value):
|
|
||||||
max_size = settings.CATALOGUE_MAX_UPLOAD_SIZE
|
|
||||||
if value.size > max_size:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
f"Image size exceeds the maximum allowed size of "
|
|
||||||
f"{max_size // (1024 * 1024)}MB."
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ..models.customers import Customer
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Customer
|
|
||||||
fields = ["id", "name", "address", "email", "phone", "external_id"]
|
|
||||||
|
|
||||||
|
|
||||||
class ListCustomerSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Customer
|
|
||||||
fields = ["id", "name"]
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ..models.payments import ReconciliationJar, PaymentMethods
|
|
||||||
from .sales import SaleSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class ReconciliationJarSerializer(serializers.ModelSerializer):
|
|
||||||
Sales = SaleSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ReconciliationJar
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"date_time",
|
|
||||||
"reconcilier",
|
|
||||||
"cash_taken",
|
|
||||||
"cash_discrepancy",
|
|
||||||
"total_cash_purchases",
|
|
||||||
"Sales",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodSerializer(serializers.Serializer):
|
|
||||||
text = serializers.CharField()
|
|
||||||
value = serializers.CharField()
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
return {
|
|
||||||
"text": instance[1],
|
|
||||||
"value": instance[0],
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ..models.products import Product, ProductCategory
|
|
||||||
|
|
||||||
|
|
||||||
class ProductSerializer(serializers.ModelSerializer):
|
|
||||||
catalogue_images = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Product
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"active",
|
|
||||||
"price",
|
|
||||||
"measuring_unit",
|
|
||||||
"categories",
|
|
||||||
"external_id",
|
|
||||||
"catalogue_images",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_catalogue_images(self, obj):
|
|
||||||
request = self.context.get("request")
|
|
||||||
if not request:
|
|
||||||
return [img.image.url for img in obj.catalogue_images.all()]
|
|
||||||
return [
|
|
||||||
request.build_absolute_uri(img.image.url)
|
|
||||||
for img in obj.catalogue_images.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ListProductSerializer(serializers.ModelSerializer):
|
|
||||||
catalogue_images = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Product
|
|
||||||
fields = ["id", "name", "catalogue_images"]
|
|
||||||
|
|
||||||
def get_catalogue_images(self, obj):
|
|
||||||
request = self.context.get("request")
|
|
||||||
if not request:
|
|
||||||
return [img.image.url for img in obj.catalogue_images.all()]
|
|
||||||
return [
|
|
||||||
request.build_absolute_uri(img.image.url)
|
|
||||||
for img in obj.catalogue_images.all()
|
|
||||||
]
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ..models.sales import (
|
|
||||||
Sale,
|
|
||||||
SaleLine,
|
|
||||||
CatalogSale,
|
|
||||||
CatalogSaleLine,
|
|
||||||
Payment,
|
|
||||||
)
|
|
||||||
from .products import ListProductSerializer
|
|
||||||
from .customers import ListCustomerSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class SaleLineSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = SaleLine
|
|
||||||
fields = ["id", "sale", "product", "unit_price", "quantity"]
|
|
||||||
|
|
||||||
|
|
||||||
class SaleSerializer(serializers.ModelSerializer):
|
|
||||||
total = serializers.ReadOnlyField(source="get_total")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Sale
|
|
||||||
fields = [
|
|
||||||
"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",
|
|
||||||
"external_id",
|
|
||||||
"customer_name",
|
|
||||||
"customer_phone",
|
|
||||||
"customer_address",
|
|
||||||
"pickup_method",
|
|
||||||
]
|
|
||||||
|
|
||||||
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 SummarySaleLineSerializer(serializers.ModelSerializer):
|
|
||||||
product = ListProductSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SaleLine
|
|
||||||
fields = ["product", "quantity", "unit_price", "description"]
|
|
||||||
|
|
||||||
|
|
||||||
class SaleSummarySerializer(serializers.ModelSerializer):
|
|
||||||
customer = ListCustomerSerializer()
|
|
||||||
lines = SummarySaleLineSerializer(many=True, source="saleline_set")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Sale
|
|
||||||
fields = ["id", "date", "customer", "payment_method", "lines"]
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
|
||||||
product = ListProductSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CatalogSaleLine
|
|
||||||
fields = ["product", "quantity", "unit_price", "description"]
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogSaleSummarySerializer(serializers.ModelSerializer):
|
|
||||||
customer = ListCustomerSerializer()
|
|
||||||
lines = CatalogSummarySaleLineSerializer(
|
|
||||||
many=True, source="catalogsaleline_set"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CatalogSale
|
|
||||||
fields = ["id", "date", "customer", "lines"]
|
|
||||||
|
|
||||||
|
|
||||||
class SaleForRenconciliationSerializer(serializers.Serializer):
|
|
||||||
id = serializers.IntegerField()
|
|
||||||
date = serializers.DateTimeField()
|
|
||||||
payment_method = serializers.CharField()
|
|
||||||
customer = serializers.SerializerMethodField()
|
|
||||||
total = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_customer(self, sale):
|
|
||||||
return {
|
|
||||||
"id": sale.customer.id,
|
|
||||||
"name": sale.customer.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_total(self, sale):
|
|
||||||
return sale.get_total()
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from .tryton import (
|
|
||||||
get_tryton_client,
|
|
||||||
ProductTrytonService,
|
|
||||||
CustomerTrytonService,
|
|
||||||
SaleTrytonService,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"get_tryton_client",
|
|
||||||
"ProductTrytonService",
|
|
||||||
"CustomerTrytonService",
|
|
||||||
"SaleTrytonService",
|
|
||||||
]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from .client import get_tryton_client, TrytonSale, TrytonLineSale
|
|
||||||
from .products import ProductTrytonService
|
|
||||||
from .customers import CustomerTrytonService
|
|
||||||
from .sales import SaleTrytonService
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"get_tryton_client",
|
|
||||||
"TrytonSale",
|
|
||||||
"TrytonLineSale",
|
|
||||||
"ProductTrytonService",
|
|
||||||
"CustomerTrytonService",
|
|
||||||
"SaleTrytonService",
|
|
||||||
]
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import os
|
|
||||||
from sabatron_tryton_rpc_client.client import Client
|
|
||||||
|
|
||||||
TRYTON_HOST = os.environ.get("TRYTON_HOST", "localhost")
|
|
||||||
TRYTON_DATABASE = os.environ.get("TRYTON_DATABASE", "tryton")
|
|
||||||
TRYTON_USERNAME = os.environ.get("TRYTON_USERNAME", "admin")
|
|
||||||
TRYTON_PASSWORD = os.environ.get("TRYTON_PASSWORD", "admin")
|
|
||||||
TRYTON_COP_CURRENCY = 31
|
|
||||||
TRYTON_COMPANY_ID = 1
|
|
||||||
TRYTON_SHOPS = [1]
|
|
||||||
|
|
||||||
|
|
||||||
def get_tryton_client():
|
|
||||||
"""Factory para crear cliente Tryton conectado"""
|
|
||||||
client = Client(
|
|
||||||
hostname=TRYTON_HOST,
|
|
||||||
database=TRYTON_DATABASE,
|
|
||||||
username=TRYTON_USERNAME,
|
|
||||||
password=TRYTON_PASSWORD,
|
|
||||||
)
|
|
||||||
client.connect()
|
|
||||||
return client
|
|
||||||
|
|
||||||
|
|
||||||
class TrytonSale:
|
|
||||||
"""Representa una venta para exportación a Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, sale, lines):
|
|
||||||
self.sale = sale
|
|
||||||
self.lines = lines
|
|
||||||
|
|
||||||
def _format_date(self, _date):
|
|
||||||
return {
|
|
||||||
"__class__": "date",
|
|
||||||
"year": _date.year,
|
|
||||||
"month": _date.month,
|
|
||||||
"day": _date.day,
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_tryton(self):
|
|
||||||
return {
|
|
||||||
"company": TRYTON_COMPANY_ID,
|
|
||||||
"shipment_address": self.sale.customer.address_external_id,
|
|
||||||
"invoice_address": self.sale.customer.address_external_id,
|
|
||||||
"currency": TRYTON_COP_CURRENCY,
|
|
||||||
"comment": self.sale.description or "",
|
|
||||||
"description": "Metodo pago: " + str(self.sale.payment_method or ""),
|
|
||||||
"party": self.sale.customer.external_id,
|
|
||||||
"reference": "don_confiao " + str(self.sale.id),
|
|
||||||
"sale_date": self._format_date(self.sale.date),
|
|
||||||
"lines": [
|
|
||||||
[
|
|
||||||
"create",
|
|
||||||
[TrytonLineSale(line).to_tryton() for line in self.lines],
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"self_pick_up": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TrytonLineSale:
|
|
||||||
"""Representa una línea de venta para exportación a Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, sale_line):
|
|
||||||
self.sale_line = sale_line
|
|
||||||
|
|
||||||
def _format_decimal(self, number):
|
|
||||||
return {"__class__": "Decimal", "decimal": str(number)}
|
|
||||||
|
|
||||||
def to_tryton(self):
|
|
||||||
return {
|
|
||||||
"product": self.sale_line.product.external_id,
|
|
||||||
"quantity": self._format_decimal(self.sale_line.quantity),
|
|
||||||
"type": "line",
|
|
||||||
"unit": self.sale_line.product.unit_external_id,
|
|
||||||
"unit_price": self._format_decimal(self.sale_line.unit_price),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TrytonCatalogSale:
|
|
||||||
"""Representa una catalog sale para exportación a Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, catalog_sale, lines):
|
|
||||||
self.catalog_sale = catalog_sale
|
|
||||||
self.lines = lines
|
|
||||||
|
|
||||||
def _format_date(self, _date):
|
|
||||||
return {
|
|
||||||
"__class__": "date",
|
|
||||||
"year": _date.year,
|
|
||||||
"month": _date.month,
|
|
||||||
"day": _date.day,
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_tryton(self):
|
|
||||||
return {
|
|
||||||
"company": TRYTON_COMPANY_ID,
|
|
||||||
"shipment_address": self.catalog_sale.customer.address_external_id,
|
|
||||||
"invoice_address": self.catalog_sale.customer.address_external_id,
|
|
||||||
"currency": TRYTON_COP_CURRENCY,
|
|
||||||
"comment": self.catalog_sale.description or "",
|
|
||||||
"description": "Venta de catálogo",
|
|
||||||
"party": self.catalog_sale.customer.external_id,
|
|
||||||
"reference": "don_confiao_catalog " + str(self.catalog_sale.id),
|
|
||||||
"sale_date": self._format_date(self.catalog_sale.date),
|
|
||||||
"lines": [
|
|
||||||
[
|
|
||||||
"create",
|
|
||||||
[TrytonCatalogSaleLine(line).to_tryton() for line in self.lines],
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"self_pick_up": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TrytonCatalogSaleLine:
|
|
||||||
"""Representa una línea de catalog sale para exportación a Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, catalog_sale_line):
|
|
||||||
self.catalog_sale_line = catalog_sale_line
|
|
||||||
|
|
||||||
def _format_decimal(self, number):
|
|
||||||
return {"__class__": "Decimal", "decimal": str(number)}
|
|
||||||
|
|
||||||
def to_tryton(self):
|
|
||||||
return {
|
|
||||||
"product": self.catalog_sale_line.product.external_id,
|
|
||||||
"quantity": self._format_decimal(self.catalog_sale_line.quantity),
|
|
||||||
"type": "line",
|
|
||||||
"unit": self.catalog_sale_line.product.unit_external_id,
|
|
||||||
"unit_price": self._format_decimal(self.catalog_sale_line.unit_price),
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
from ...models.customers import Customer
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerTrytonService:
|
|
||||||
"""Servicio para sincronización de clientes con Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, tryton_client):
|
|
||||||
self.client = tryton_client
|
|
||||||
|
|
||||||
def import_from_tryton(self):
|
|
||||||
"""Importa clientes desde Tryton"""
|
|
||||||
method = "model.party.party.search"
|
|
||||||
context = {"company": 1}
|
|
||||||
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
|
|
||||||
party_ids = self.client.call(method, params)
|
|
||||||
tryton_parties = self._get_party_details(party_ids, context)
|
|
||||||
|
|
||||||
checked_tryton_parties = party_ids
|
|
||||||
failed_parties = []
|
|
||||||
updated_customers = []
|
|
||||||
created_customers = []
|
|
||||||
untouched_customers = []
|
|
||||||
|
|
||||||
for tryton_party in tryton_parties:
|
|
||||||
try:
|
|
||||||
customer = Customer.objects.get(external_id=tryton_party.get("id"))
|
|
||||||
except Customer.DoesNotExist:
|
|
||||||
customer = self._create_customer(tryton_party)
|
|
||||||
created_customers.append(customer.id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self._need_update(customer, tryton_party):
|
|
||||||
self._update_customer(customer, tryton_party)
|
|
||||||
updated_customers.append(customer.id)
|
|
||||||
else:
|
|
||||||
untouched_customers.append(customer.id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"checked_tryton_parties": checked_tryton_parties,
|
|
||||||
"failed_parties": failed_parties,
|
|
||||||
"updated_customers": updated_customers,
|
|
||||||
"created_customers": created_customers,
|
|
||||||
"untouched_customers": untouched_customers,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_party_details(self, party_ids, context):
|
|
||||||
"""Obtiene detalles de clientes desde Tryton"""
|
|
||||||
tryton_fields = ["id", "name", "addresses"]
|
|
||||||
method = "model.party.party.read"
|
|
||||||
params = (party_ids, tryton_fields, context)
|
|
||||||
response = self.client.call(method, params)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _need_update(self, customer, tryton_party):
|
|
||||||
"""Verifica si el cliente necesita actualización"""
|
|
||||||
if not customer.name == tryton_party.get("name"):
|
|
||||||
return True
|
|
||||||
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
|
||||||
if not customer.address_external_id == str(
|
|
||||||
tryton_party.get("addresses")[0]
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _create_customer(self, tryton_party):
|
|
||||||
"""Crea un nuevo cliente desde datos de Tryton"""
|
|
||||||
customer = Customer()
|
|
||||||
customer.name = tryton_party.get("name")
|
|
||||||
customer.external_id = tryton_party.get("id")
|
|
||||||
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
|
||||||
customer.address_external_id = tryton_party.get("addresses")[0]
|
|
||||||
customer.save()
|
|
||||||
return customer
|
|
||||||
|
|
||||||
def _update_customer(self, customer, tryton_party):
|
|
||||||
"""Actualiza un cliente existente con datos de Tryton"""
|
|
||||||
customer.name = tryton_party.get("name")
|
|
||||||
customer.external_id = tryton_party.get("id")
|
|
||||||
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
|
||||||
customer.address_external_id = tryton_party.get("addresses")[0]
|
|
||||||
customer.save()
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
from ...models.products import Product
|
|
||||||
|
|
||||||
|
|
||||||
class ProductTrytonService:
|
|
||||||
"""Servicio para sincronización de productos con Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, tryton_client):
|
|
||||||
self.client = tryton_client
|
|
||||||
|
|
||||||
def import_from_tryton(self):
|
|
||||||
"""Importa productos desde Tryton"""
|
|
||||||
method = "model.product.product.search"
|
|
||||||
context = {"company": 1}
|
|
||||||
params = [
|
|
||||||
[["salable", "=", True]],
|
|
||||||
0,
|
|
||||||
1000,
|
|
||||||
[["rec_name", "ASC"], ["id", None]],
|
|
||||||
context,
|
|
||||||
]
|
|
||||||
product_ids = self.client.call(method, params)
|
|
||||||
tryton_products = self._get_product_details(product_ids, context)
|
|
||||||
|
|
||||||
checked_tryton_products = product_ids
|
|
||||||
failed_products = []
|
|
||||||
updated_products = []
|
|
||||||
created_products = []
|
|
||||||
untouched_products = []
|
|
||||||
|
|
||||||
for tryton_product in tryton_products:
|
|
||||||
try:
|
|
||||||
product = Product.objects.get(external_id=tryton_product.get("id"))
|
|
||||||
except Product.DoesNotExist:
|
|
||||||
try:
|
|
||||||
product = self._create_product(tryton_product)
|
|
||||||
created_products.append(product.id)
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"Error al importar productos: {e}El producto: {tryton_product}"
|
|
||||||
)
|
|
||||||
failed_products.append(tryton_product.get("id"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self._need_update(product, tryton_product):
|
|
||||||
self._update_product(product, tryton_product)
|
|
||||||
updated_products.append(product.id)
|
|
||||||
else:
|
|
||||||
untouched_products.append(product.id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"checked_tryton_products": checked_tryton_products,
|
|
||||||
"failed_products": failed_products,
|
|
||||||
"updated_products": updated_products,
|
|
||||||
"created_products": created_products,
|
|
||||||
"untouched_products": untouched_products,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_product_details(self, product_ids, context):
|
|
||||||
"""Obtiene detalles de productos desde Tryton"""
|
|
||||||
tryton_fields = [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"default_uom.id",
|
|
||||||
"default_uom.rec_name",
|
|
||||||
"list_price",
|
|
||||||
]
|
|
||||||
method = "model.product.product.read"
|
|
||||||
params = (product_ids, tryton_fields, context)
|
|
||||||
response = self.client.call(method, params)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _need_update(self, product, tryton_product):
|
|
||||||
"""Verifica si el producto necesita actualización"""
|
|
||||||
if not product.name == tryton_product.get("name"):
|
|
||||||
return True
|
|
||||||
if not product.price == tryton_product.get("list_price"):
|
|
||||||
return True
|
|
||||||
unit = tryton_product.get("default_uom.")
|
|
||||||
if not product.measuring_unit == unit.get("rec_name"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _create_product(self, tryton_product):
|
|
||||||
"""Crea un nuevo producto desde datos de Tryton"""
|
|
||||||
product = Product()
|
|
||||||
product.name = tryton_product.get("name")
|
|
||||||
product.price = tryton_product.get("list_price")
|
|
||||||
product.external_id = tryton_product.get("id")
|
|
||||||
unit = tryton_product.get("default_uom.")
|
|
||||||
product.measuring_unit = unit.get("rec_name")
|
|
||||||
product.unit_external_id = unit.get("id")
|
|
||||||
product.save()
|
|
||||||
return product
|
|
||||||
|
|
||||||
def _update_product(self, product, tryton_product):
|
|
||||||
"""Actualiza un producto existente con datos de Tryton"""
|
|
||||||
product.name = tryton_product.get("name")
|
|
||||||
product.price = tryton_product.get("list_price")
|
|
||||||
product.external_id = tryton_product.get("id")
|
|
||||||
unit = tryton_product.get("default_uom.")
|
|
||||||
product.measuring_unit = unit.get("rec_name")
|
|
||||||
product.unit_external_id = unit.get("id")
|
|
||||||
product.save()
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
from ...models.sales import Sale, SaleLine, CatalogSale, CatalogSaleLine
|
|
||||||
from .client import TrytonSale, TrytonCatalogSale, TRYTON_COMPANY_ID, TRYTON_SHOPS
|
|
||||||
|
|
||||||
|
|
||||||
class SaleTrytonService:
|
|
||||||
"""Servicio para sincronización de ventas con Tryton"""
|
|
||||||
|
|
||||||
def __init__(self, tryton_client):
|
|
||||||
self.client = tryton_client
|
|
||||||
|
|
||||||
def send_to_tryton(self):
|
|
||||||
"""Envía ventas sin external_id a Tryton"""
|
|
||||||
method = "model.sale.sale.create"
|
|
||||||
tryton_context = {
|
|
||||||
"company": TRYTON_COMPANY_ID,
|
|
||||||
"shops": TRYTON_SHOPS,
|
|
||||||
}
|
|
||||||
|
|
||||||
successful = []
|
|
||||||
failed = []
|
|
||||||
|
|
||||||
sales = Sale.objects.filter(external_id=None)
|
|
||||||
for sale in sales:
|
|
||||||
try:
|
|
||||||
lines = SaleLine.objects.filter(sale=sale.id)
|
|
||||||
tryton_params = self._to_tryton_params(sale, lines, tryton_context)
|
|
||||||
external_ids = self.client.call(method, tryton_params)
|
|
||||||
sale.external_id = external_ids[0]
|
|
||||||
sale.save()
|
|
||||||
successful.append(sale.id)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error al enviar la venta: {e}venta_id: {sale.id}")
|
|
||||||
failed.append(sale.id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {"successful": successful, "failed": failed}
|
|
||||||
|
|
||||||
def _to_tryton_params(self, sale, lines, tryton_context):
|
|
||||||
"""Convierte venta a parámetros para Tryton"""
|
|
||||||
sale_tryton = TrytonSale(sale, lines)
|
|
||||||
return [[sale_tryton.to_tryton()], tryton_context]
|
|
||||||
|
|
||||||
def send_catalog_sales_to_tryton(self):
|
|
||||||
"""Envía catalog sales sin external_id a Tryton"""
|
|
||||||
method = "model.sale.sale.create"
|
|
||||||
tryton_context = {
|
|
||||||
"company": TRYTON_COMPANY_ID,
|
|
||||||
"shops": TRYTON_SHOPS,
|
|
||||||
}
|
|
||||||
|
|
||||||
successful = []
|
|
||||||
failed = []
|
|
||||||
|
|
||||||
catalog_sales = CatalogSale.objects.filter(external_id=None)
|
|
||||||
for catalog_sale in catalog_sales:
|
|
||||||
try:
|
|
||||||
lines = CatalogSaleLine.objects.filter(catalog_sale=catalog_sale.id)
|
|
||||||
tryton_params = self._catalog_sale_to_tryton_params(
|
|
||||||
catalog_sale, lines, tryton_context
|
|
||||||
)
|
|
||||||
external_ids = self.client.call(method, tryton_params)
|
|
||||||
catalog_sale.external_id = external_ids[0]
|
|
||||||
catalog_sale.save()
|
|
||||||
successful.append(catalog_sale.id)
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"Error al enviar catalog sale: {e}, catalog_sale_id: {catalog_sale.id}"
|
|
||||||
)
|
|
||||||
failed.append(catalog_sale.id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {"successful": successful, "failed": failed}
|
|
||||||
|
|
||||||
def _catalog_sale_to_tryton_params(self, catalog_sale, lines, tryton_context):
|
|
||||||
"""Convierte catalog sale a parámetros para Tryton"""
|
|
||||||
sale_tryton = TrytonCatalogSale(catalog_sale, lines)
|
|
||||||
return [[sale_tryton.to_tryton()], tryton_context]
|
|
||||||
17
tienda_ilusion/don_confiao/templates/don_confiao/base.html
Normal file
17
tienda_ilusion/don_confiao/templates/don_confiao/base.html
Normal 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>
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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>
|
||||||
17
tienda_ilusion/don_confiao/templates/don_confiao/menu.html
Normal file
17
tienda_ilusion/don_confiao/templates/don_confiao/menu.html
Normal 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>
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,132 +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_catalog_sale_summary(self):
|
|
||||||
# Create a catalog sale
|
|
||||||
response = self._create_catalog_sale()
|
|
||||||
content = json.loads(response.content.decode("utf-8"))
|
|
||||||
catalog_sale_id = content["id"]
|
|
||||||
|
|
||||||
# Get the summary
|
|
||||||
url = f"/don_confiao/resumen_compra_catalogo_json/{catalog_sale_id}"
|
|
||||||
response = self.client.get(url)
|
|
||||||
|
|
||||||
# Verify response
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
json_response = json.loads(response.content.decode("utf-8"))
|
|
||||||
|
|
||||||
# Verify structure
|
|
||||||
self.assertIn("id", json_response)
|
|
||||||
self.assertIn("date", json_response)
|
|
||||||
self.assertIn("customer", json_response)
|
|
||||||
self.assertIn("lines", json_response)
|
|
||||||
|
|
||||||
# Verify customer details
|
|
||||||
self.assertEqual(json_response["customer"]["id"], self.customer.id)
|
|
||||||
self.assertEqual(json_response["customer"]["name"], self.customer.name)
|
|
||||||
|
|
||||||
# Verify lines
|
|
||||||
self.assertEqual(len(json_response["lines"]), 2)
|
|
||||||
|
|
||||||
# Verify first line
|
|
||||||
line1 = json_response["lines"][0]
|
|
||||||
self.assertIn("product", line1)
|
|
||||||
self.assertIn("quantity", line1)
|
|
||||||
self.assertIn("unit_price", line1)
|
|
||||||
self.assertEqual(line1["product"]["id"], self.product.id)
|
|
||||||
self.assertEqual(line1["product"]["name"], self.product.name)
|
|
||||||
self.assertEqual(line1["quantity"], "2.00")
|
|
||||||
self.assertEqual(line1["unit_price"], "3000.00")
|
|
||||||
|
|
||||||
# Verify second line
|
|
||||||
line2 = json_response["lines"][1]
|
|
||||||
self.assertEqual(line2["product"]["id"], self.product.id)
|
|
||||||
self.assertEqual(line2["quantity"], "3.00")
|
|
||||||
self.assertEqual(line2["unit_price"], "5000.00")
|
|
||||||
|
|
||||||
def test_catalog_sale_has_external_id_field(self):
|
|
||||||
"""Verifica que CatalogSale tiene el campo external_id"""
|
|
||||||
response = self._create_catalog_sale()
|
|
||||||
content = json.loads(response.content.decode("utf-8"))
|
|
||||||
catalog_sale_id = content["id"]
|
|
||||||
|
|
||||||
catalog_sale = CatalogSale.objects.get(pk=catalog_sale_id)
|
|
||||||
# Debe tener el campo external_id
|
|
||||||
self.assertIsNone(catalog_sale.external_id)
|
|
||||||
|
|
||||||
# Se puede asignar un valor
|
|
||||||
catalog_sale.external_id = "123"
|
|
||||||
catalog_sale.save()
|
|
||||||
|
|
||||||
# Verificar que se guardó
|
|
||||||
catalog_sale.refresh_from_db()
|
|
||||||
self.assertEqual(catalog_sale.external_id, "123")
|
|
||||||
|
|
||||||
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",
|
||||||
@@ -173,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')
|
||||||
|
|||||||
22
tienda_ilusion/don_confiao/tests/test_buy_form.py
Normal file
22
tienda_ilusion/don_confiao/tests/test_buy_form.py
Normal 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)
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
import io
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
from django.test import override_settings
|
|
||||||
from PIL import Image
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.test import APIClient, APITestCase
|
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
|
||||||
|
|
||||||
from ..models.catalogue_images import CatalogueImage
|
|
||||||
from ..models.products import Product
|
|
||||||
from .Mixins import LoginMixin
|
|
||||||
|
|
||||||
|
|
||||||
def _create_test_image(width=600, height=400, name="test.png"):
|
|
||||||
"""Create a SimpleUploadedFile from an in-memory image using PIL."""
|
|
||||||
img = Image.new("RGBA", (width, height), (255, 0, 0, 255))
|
|
||||||
output = io.BytesIO()
|
|
||||||
img.save(output, format="PNG")
|
|
||||||
return SimpleUploadedFile(
|
|
||||||
name=name,
|
|
||||||
content=output.getvalue(),
|
|
||||||
content_type="image/png",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCatalogueImageModel(APITestCase, LoginMixin):
|
|
||||||
def setUp(self):
|
|
||||||
self.login()
|
|
||||||
self.product = Product.objects.create(
|
|
||||||
name="Test Product", price=100.00
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_catalogue_image(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
catalogue_image = CatalogueImage.objects.create(
|
|
||||||
product=self.product, image=image_file
|
|
||||||
)
|
|
||||||
self.assertIsInstance(catalogue_image, CatalogueImage)
|
|
||||||
self.assertEqual(catalogue_image.product, self.product)
|
|
||||||
self.assertIsNotNone(catalogue_image.image)
|
|
||||||
self.assertIsNotNone(catalogue_image.uploaded_at)
|
|
||||||
|
|
||||||
def test_catalogue_image_product_relation(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
ci = CatalogueImage.objects.create(
|
|
||||||
product=self.product, image=image_file
|
|
||||||
)
|
|
||||||
self.assertIn(ci, self.product.catalogue_images.all())
|
|
||||||
|
|
||||||
def test_catalogue_image_str(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
ci = CatalogueImage.objects.create(
|
|
||||||
product=self.product, image=image_file
|
|
||||||
)
|
|
||||||
expected = f"CatalogueImage for {self.product.name} ({ci.id})"
|
|
||||||
self.assertEqual(str(ci), expected)
|
|
||||||
|
|
||||||
def test_cascade_delete_with_product(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
CatalogueImage.objects.create(
|
|
||||||
product=self.product, image=image_file
|
|
||||||
)
|
|
||||||
self.assertEqual(CatalogueImage.objects.count(), 1)
|
|
||||||
self.product.delete()
|
|
||||||
self.assertEqual(CatalogueImage.objects.count(), 0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCatalogueImageAPIPermissions(APITestCase, LoginMixin):
|
|
||||||
def setUp(self):
|
|
||||||
self.product = Product.objects.create(
|
|
||||||
name="Perm Test Product", price=100.00
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_catalogue_image_unauthenticated(self):
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
image_file = _create_test_image()
|
|
||||||
data = {"product": self.product.id, "image": image_file}
|
|
||||||
response = self.client.post(url, data, format="multipart")
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
||||||
|
|
||||||
def test_create_catalogue_image_non_admin(self):
|
|
||||||
self.user = User.objects.create_user(
|
|
||||||
username="regularuser",
|
|
||||||
email="regular@example.com",
|
|
||||||
password="regularpass",
|
|
||||||
)
|
|
||||||
refresh = RefreshToken.for_user(self.user)
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.credentials(
|
|
||||||
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
image_file = _create_test_image()
|
|
||||||
data = {"product": self.product.id, "image": image_file}
|
|
||||||
response = self.client.post(url, data, format="multipart")
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
def test_get_catalogue_images_authenticated_non_admin(self):
|
|
||||||
self.user = User.objects.create_user(
|
|
||||||
username="regularuser2",
|
|
||||||
email="regular2@example.com",
|
|
||||||
password="regularpass",
|
|
||||||
)
|
|
||||||
refresh = RefreshToken.for_user(self.user)
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.credentials(
|
|
||||||
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_delete_catalogue_image_non_admin(self):
|
|
||||||
admin_user = User.objects.create_superuser(
|
|
||||||
username="admin2", email="admin2@example.com", password="adminpass"
|
|
||||||
)
|
|
||||||
admin_refresh = RefreshToken.for_user(admin_user)
|
|
||||||
admin_client = APIClient()
|
|
||||||
admin_client.credentials(
|
|
||||||
HTTP_AUTHORIZATION=f"Bearer {str(admin_refresh.access_token)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
image_file = _create_test_image()
|
|
||||||
create_data = {
|
|
||||||
"product": self.product.id, "image": image_file
|
|
||||||
}
|
|
||||||
create_response = admin_client.post(
|
|
||||||
"/don_confiao/api/catalogue_images/", create_data, format="multipart"
|
|
||||||
)
|
|
||||||
ci_id = create_response.json()["id"]
|
|
||||||
|
|
||||||
regular_user = User.objects.create_user(
|
|
||||||
username="regularuser3",
|
|
||||||
email="regular3@example.com",
|
|
||||||
password="regularpass",
|
|
||||||
)
|
|
||||||
refresh = RefreshToken.for_user(regular_user)
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.credentials(
|
|
||||||
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.delete(
|
|
||||||
f"/don_confiao/api/catalogue_images/{ci_id}/"
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
self.assertEqual(CatalogueImage.objects.count(), 1)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCatalogueImageAPI(APITestCase, LoginMixin):
|
|
||||||
def setUp(self):
|
|
||||||
self.login()
|
|
||||||
self.product = Product.objects.create(
|
|
||||||
name="API Test Product", price=100.00
|
|
||||||
)
|
|
||||||
self.product2 = Product.objects.create(
|
|
||||||
name="API Test Product 2", price=200.00
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_image(self, product=None, width=600, height=400):
|
|
||||||
if product is None:
|
|
||||||
product = self.product
|
|
||||||
image_file = _create_test_image(width=width, height=height)
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
data = {"product": product.id, "image": image_file}
|
|
||||||
return self.client.post(url, data, format="multipart")
|
|
||||||
|
|
||||||
def test_list_catalogue_images_empty(self):
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_create_catalogue_image(self):
|
|
||||||
response = self._create_image()
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
||||||
data = response.json()
|
|
||||||
self.assertIn("id", data)
|
|
||||||
self.assertEqual(data["product"], self.product.id)
|
|
||||||
self.assertIn("image", data)
|
|
||||||
self.assertIn("uploaded_at", data)
|
|
||||||
|
|
||||||
def test_list_catalogue_images(self):
|
|
||||||
self._create_image()
|
|
||||||
self._create_image(product=self.product2)
|
|
||||||
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(len(response.json()), 2)
|
|
||||||
|
|
||||||
def test_retrieve_catalogue_image(self):
|
|
||||||
create_response = self._create_image()
|
|
||||||
ci_id = create_response.json()["id"]
|
|
||||||
|
|
||||||
url = f"/don_confiao/api/catalogue_images/{ci_id}/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.json()["id"], ci_id)
|
|
||||||
self.assertEqual(response.json()["product"], self.product.id)
|
|
||||||
|
|
||||||
def test_update_catalogue_image(self):
|
|
||||||
create_response = self._create_image()
|
|
||||||
ci_id = create_response.json()["id"]
|
|
||||||
|
|
||||||
new_image = _create_test_image(width=200, height=200, name="updated.png")
|
|
||||||
url = f"/don_confiao/api/catalogue_images/{ci_id}/"
|
|
||||||
response = self.client.put(
|
|
||||||
url,
|
|
||||||
{"product": self.product.id, "image": new_image},
|
|
||||||
format="multipart",
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_delete_catalogue_image(self):
|
|
||||||
create_response = self._create_image()
|
|
||||||
ci_id = create_response.json()["id"]
|
|
||||||
|
|
||||||
url = f"/don_confiao/api/catalogue_images/{ci_id}/"
|
|
||||||
response = self.client.delete(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
self.assertEqual(CatalogueImage.objects.count(), 0)
|
|
||||||
|
|
||||||
def test_create_catalogue_image_invalid_product(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
data = {"product": 99999, "image": image_file}
|
|
||||||
response = self.client.post(url, data, format="multipart")
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_create_catalogue_image_missing_file(self):
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
data = {"product": self.product.id}
|
|
||||||
response = self.client.post(url, data, format="multipart")
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCatalogueImageResize(APITestCase, LoginMixin):
|
|
||||||
def setUp(self):
|
|
||||||
self.login()
|
|
||||||
self.product = Product.objects.create(
|
|
||||||
name="Resize Test Product", price=100.00
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_and_get_image_obj(self, width, height, strict=False,
|
|
||||||
bg_color=(0, 0, 0, 0)):
|
|
||||||
settings_overrides = {
|
|
||||||
"CATALOGUE_IMAGE_WIDTH": 600,
|
|
||||||
"CATALOGUE_IMAGE_HEIGHT": 600,
|
|
||||||
"CATALOGUE_STRICT_DIMENSION": strict,
|
|
||||||
"CATALOGUE_BACKGROUND_IMAGES_RGBA": bg_color,
|
|
||||||
}
|
|
||||||
|
|
||||||
with override_settings(**settings_overrides):
|
|
||||||
image_file = _create_test_image(width=width, height=height)
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
data = {"product": self.product.id, "image": image_file}
|
|
||||||
response = self.client.post(url, data, format="multipart")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
||||||
ci_id = response.json()["id"]
|
|
||||||
return CatalogueImage.objects.get(id=ci_id)
|
|
||||||
|
|
||||||
def test_resize_non_strict_mode_landscape(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=800, height=600, strict=False
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 600)
|
|
||||||
expected_height = int(600 * 600 / 800)
|
|
||||||
self.assertEqual(img.height, expected_height)
|
|
||||||
|
|
||||||
def test_resize_non_strict_mode_portrait(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=600, height=800, strict=False
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 600)
|
|
||||||
expected_height = int(800 * 600 / 600)
|
|
||||||
self.assertEqual(img.height, expected_height)
|
|
||||||
|
|
||||||
def test_resize_non_strict_mode_square(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=600, height=600, strict=False
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 600)
|
|
||||||
self.assertEqual(img.height, 600)
|
|
||||||
|
|
||||||
def test_resize_non_strict_no_upscale(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=400, height=300, strict=False
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 400)
|
|
||||||
self.assertEqual(img.height, 300)
|
|
||||||
|
|
||||||
def test_resize_strict_mode_landscape(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=800, height=600, strict=True
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 600)
|
|
||||||
self.assertEqual(img.height, 600)
|
|
||||||
|
|
||||||
def test_resize_strict_mode_portrait(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=600, height=800, strict=True
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 600)
|
|
||||||
self.assertEqual(img.height, 600)
|
|
||||||
|
|
||||||
def test_resize_strict_mode_with_color_background(self):
|
|
||||||
ci = self._create_and_get_image_obj(
|
|
||||||
width=800, height=600, strict=True, bg_color=(255, 255, 255, 255)
|
|
||||||
)
|
|
||||||
with Image.open(ci.image.path) as img:
|
|
||||||
self.assertEqual(img.width, 600)
|
|
||||||
self.assertEqual(img.height, 600)
|
|
||||||
|
|
||||||
@override_settings(CATALOGUE_MAX_UPLOAD_SIZE=1)
|
|
||||||
def test_resize_large_file_rejected(self):
|
|
||||||
image_file = _create_test_image(width=10, height=10)
|
|
||||||
url = "/don_confiao/api/catalogue_images/"
|
|
||||||
data = {"product": self.product.id, "image": image_file}
|
|
||||||
response = self.client.post(url, data, format="multipart")
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
class TestProductListingWithImages(APITestCase, LoginMixin):
|
|
||||||
def setUp(self):
|
|
||||||
self.login()
|
|
||||||
self.product = Product.objects.create(
|
|
||||||
name="Product With Images", price=100.00
|
|
||||||
)
|
|
||||||
self.product_no_images = Product.objects.create(
|
|
||||||
name="Product Without Images", price=200.00
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_list_includes_catalogue_images_field(self):
|
|
||||||
url = "/don_confiao/api/products/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
for product in response.json():
|
|
||||||
self.assertIn("catalogue_images", product)
|
|
||||||
|
|
||||||
def test_product_list_catalogue_images_urls(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
CatalogueImage.objects.create(
|
|
||||||
product=self.product, image=image_file
|
|
||||||
)
|
|
||||||
|
|
||||||
url = "/don_confiao/api/products/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
product_data = next(
|
|
||||||
p for p in data if p["id"] == self.product.id
|
|
||||||
)
|
|
||||||
self.assertEqual(len(product_data["catalogue_images"]), 1)
|
|
||||||
self.assertTrue(
|
|
||||||
product_data["catalogue_images"][0].endswith(".png")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_list_catalogue_images_multiple(self):
|
|
||||||
img1 = _create_test_image(name="img1.png")
|
|
||||||
img2 = _create_test_image(name="img2.png")
|
|
||||||
CatalogueImage.objects.create(product=self.product, image=img1)
|
|
||||||
CatalogueImage.objects.create(product=self.product, image=img2)
|
|
||||||
|
|
||||||
url = "/don_confiao/api/products/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
product_data = next(
|
|
||||||
p for p in data if p["id"] == self.product.id
|
|
||||||
)
|
|
||||||
self.assertEqual(len(product_data["catalogue_images"]), 2)
|
|
||||||
|
|
||||||
def test_product_list_catalogue_images_empty(self):
|
|
||||||
url = "/don_confiao/api/products/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
product_data = next(
|
|
||||||
p for p in data if p["id"] == self.product_no_images.id
|
|
||||||
)
|
|
||||||
self.assertEqual(product_data["catalogue_images"], [])
|
|
||||||
|
|
||||||
def test_product_detail_includes_catalogue_images(self):
|
|
||||||
image_file = _create_test_image()
|
|
||||||
CatalogueImage.objects.create(
|
|
||||||
product=self.product, image=image_file
|
|
||||||
)
|
|
||||||
|
|
||||||
url = f"/don_confiao/api/products/{self.product.id}/"
|
|
||||||
response = self.client.get(url)
|
|
||||||
data = response.json()
|
|
||||||
self.assertIn("catalogue_images", data)
|
|
||||||
self.assertEqual(len(data["catalogue_images"]), 1)
|
|
||||||
@@ -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))
|
||||||
|
|||||||
50
tienda_ilusion/don_confiao/tests/test_export_sales.py
Normal file
50
tienda_ilusion/don_confiao/tests/test_export_sales.py
Normal 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"
|
||||||
|
]
|
||||||
@@ -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]}])
|
||||||
@@ -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",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
45
tienda_ilusion/don_confiao/tests/tests_purchase_form.py
Normal file
45
tienda_ilusion/don_confiao/tests/tests_purchase_form.py
Normal 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())
|
||||||
@@ -2,97 +2,41 @@ from django.urls import path, include
|
|||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from .api import (
|
from . import api_views
|
||||||
# Catalogue Images
|
|
||||||
CatalogueImageViewSet,
|
|
||||||
# Products
|
|
||||||
ProductView,
|
|
||||||
ProductsFromTrytonView,
|
|
||||||
# Customers
|
|
||||||
CustomerView,
|
|
||||||
CustomersFromTrytonView,
|
|
||||||
# Sales
|
|
||||||
SaleView,
|
|
||||||
CatalogSaleView,
|
|
||||||
SaleSummary,
|
|
||||||
CatalogSaleSummary,
|
|
||||||
SalesForTrytonView,
|
|
||||||
SalesToTrytonView,
|
|
||||||
CatalogSalesToTrytonView,
|
|
||||||
# Payments
|
|
||||||
ReconciliateJarView,
|
|
||||||
ReconciliateJarModelView,
|
|
||||||
PaymentMethodView,
|
|
||||||
SalesForReconciliationView,
|
|
||||||
# Admin
|
|
||||||
AdminCodeValidateView,
|
|
||||||
)
|
|
||||||
|
|
||||||
app_name = "don_confiao"
|
app_name = 'don_confiao'
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r"sales", SaleView, basename="sale")
|
router.register(r'sales', api_views.SaleView, basename='sale')
|
||||||
router.register(r"catalog_sales", CatalogSaleView, basename="catalog_sale")
|
router.register(r'customers', api_views.CustomerView, basename='customer')
|
||||||
router.register(r"customers", CustomerView, basename="customer")
|
router.register(r'products', api_views.ProductView, basename='product')
|
||||||
router.register(r"products", ProductView, basename="product")
|
router.register(r'reconciliate_jar', api_views.ReconciliateJarModelView,
|
||||||
router.register(
|
basename='reconciliate_jar')
|
||||||
r"catalogue_images",
|
|
||||||
CatalogueImageViewSet,
|
|
||||||
basename="catalogue_image",
|
|
||||||
)
|
|
||||||
router.register(
|
|
||||||
r"reconciliate_jar",
|
|
||||||
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"),
|
||||||
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"),
|
||||||
"resumen_compra_catalogo_json/<int:id>",
|
path('api/importar_clientes_de_tryton',
|
||||||
CatalogSaleSummary.as_view(),
|
api_views.CustomersFromTrytonView.as_view(),
|
||||||
name="catalog_purchase_json_summary",
|
name="customers_from_tryton"),
|
||||||
),
|
path("exportar_ventas_para_tryton",
|
||||||
path(
|
views.exportar_ventas_para_tryton,
|
||||||
"payment_methods/all/select_format",
|
name="exportar_ventas_para_tryton"),
|
||||||
PaymentMethodView.as_view(),
|
path('api/enviar_ventas_a_tryton', api_views.SalesToTrytonView.as_view(), name="send_tryton"),
|
||||||
name="payment_methods_to_select",
|
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(
|
path("payment_methods/all/select_format", api_views.PaymentMethodView.as_view(), name="payment_methods_to_select"),
|
||||||
"purchases/for_reconciliation",
|
path('purchases/for_reconciliation', api_views.SalesForReconciliationView.as_view(), name='sales_for_reconciliation'),
|
||||||
SalesForReconciliationView.as_view(),
|
path('reconciliate_jar', api_views.ReconciliateJarView.as_view()),
|
||||||
name="sales_for_reconciliation",
|
path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()),
|
||||||
),
|
path('api/sales/for_tryton', api_views.SalesForTrytonView.as_view()),
|
||||||
path("reconciliate_jar", ReconciliateJarView.as_view()),
|
path('api/', include(router.urls)),
|
||||||
path("api/", include(router.urls)),
|
|
||||||
path(
|
|
||||||
"api/importar_productos_de_tryton",
|
|
||||||
ProductsFromTrytonView.as_view(),
|
|
||||||
name="products_from_tryton",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/importar_clientes_de_tryton",
|
|
||||||
CustomersFromTrytonView.as_view(),
|
|
||||||
name="customers_from_tryton",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/enviar_ventas_a_tryton",
|
|
||||||
SalesToTrytonView.as_view(),
|
|
||||||
name="send_tryton",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/enviar_catalog_sales_a_tryton",
|
|
||||||
CatalogSalesToTrytonView.as_view(),
|
|
||||||
name="send_catalog_sales_tryton",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/admin_code/validate/<code>",
|
|
||||||
AdminCodeValidateView.as_view(),
|
|
||||||
),
|
|
||||||
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
157
tienda_ilusion/tienda_ilusion/settings.py
Normal file
157
tienda_ilusion/tienda_ilusion/settings.py
Normal 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",
|
||||||
|
# ]
|
||||||
@@ -14,8 +14,6 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls.static import static
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework_simplejwt.views import (
|
from rest_framework_simplejwt.views import (
|
||||||
@@ -34,8 +32,3 @@ urlpatterns = [
|
|||||||
name='token_refresh'),
|
name='token_refresh'),
|
||||||
path('api/users/', include('users.urls')),
|
path('api/users/', include('users.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Serve media files through Django only in development.
|
|
||||||
# In production/staging, media is served by the web server (nginx).
|
|
||||||
if settings.DEBUG:
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user