Compare commits
15 Commits
f65b85c7e7
...
feat/seria
| Author | SHA1 | Date | |
|---|---|---|---|
| b088794716 | |||
| 177b4c01ca | |||
| ea3727363b | |||
| bee35ac5cb | |||
| 85168e24f0 | |||
| 56bbc8f9b4 | |||
|
|
805565aa8e | ||
|
|
4f41156056 | ||
|
|
2438cf2576 | ||
|
|
332884907c | ||
|
|
b2312edff5 | ||
| 4410cc919a | |||
| d1c2b4f1bb | |||
| d57b827e4e | |||
| 1e32cfce7f |
39
AGENTS.md
39
AGENTS.md
@@ -159,7 +159,44 @@ 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 -f docker-compose.dev.yml run --rm django python manage.py test`
|
||||||
|
|
||||||
|
## Workflow TDD (Semáforo Rojo/Verde)
|
||||||
|
|
||||||
|
Todo cambio o nueva funcionalidad debe seguir TDD. **Está prohibido asumir fallos de tests: todo fallo debe ser confirmado ejecutando los tests.**
|
||||||
|
|
||||||
|
### Regla de oro: confirmar antes de modificar
|
||||||
|
Antes de modificar tests (por cambios en comportamiento o por nuevas funcionalidades), **SIEMPRE ejecutar los tests primero** y confirmar que el fallo esperado realmente ocurre. Nunca asumir que un test falla: verificar el fallo con la ejecución real.
|
||||||
|
|
||||||
|
### Ciclo Rojo → Verde → Refactor
|
||||||
|
1. **ROJO (escribir el test y confirmar que falla)**:
|
||||||
|
- Escribir o modificar el test que describe el comportamiento esperado.
|
||||||
|
- Ejecutar los tests **antes de tocar el código de producción**.
|
||||||
|
- Confirmar que el test falla por el motivo esperado (assertion fallida por el comportamiento nuevo, no por error de setup o de importación).
|
||||||
|
- Copiar el output del fallo como evidencia del semáforo rojo.
|
||||||
|
|
||||||
|
2. **VERDE (implementar lo mínimo para que pase)**:
|
||||||
|
- Implementar el código de producción mínimo necesario para que el test pase.
|
||||||
|
- Volver a ejecutar los tests y confirmar que el semáforo queda en verde.
|
||||||
|
|
||||||
|
3. **REFACTOR (limpiar con los tests en verde)**:
|
||||||
|
- Refactorizar y limpiar el código sin cambiar comportamiento.
|
||||||
|
- Re-ejecutar los tests para confirmar que siguen en verde.
|
||||||
|
|
||||||
|
### Ejecutar tests
|
||||||
|
Siempre ejecutar dentro del contenedor de desarrollo:
|
||||||
|
```bash
|
||||||
|
# Test específico (durante el ciclo, preferible sobre el suite completo para ser rápido)
|
||||||
|
docker compose -f docker-compose.dev.yml run --rm django python manage.py test don_confiao.tests.<archivo>
|
||||||
|
|
||||||
|
# Suite completa (al finalizar, para confirmar que nada se rompió)
|
||||||
|
docker compose -f docker-compose.dev.yml run --rm django python manage.py test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Confirmar el fallo, no asumirlo
|
||||||
|
- Si un test falla o se espera que falle, **ejecutarlo y mostrar el resultado** antes de cambiar cualquier código o test.
|
||||||
|
- Verificar que el fallo corresponde a la razón esperada (leer el traceback), no a un problema de entorno, migraciones, importaciones o fixtures.
|
||||||
|
- No modificar tests para "hacerlos pasar" si el fallo no fue previamente confirmado.
|
||||||
|
|
||||||
## 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 -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`
|
||||||
|
|||||||
80
scripts/fill_sale_codes.py
Normal file
80
scripts/fill_sale_codes.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN_URL = "/api/token/"
|
||||||
|
FILL_SALES_CODES_URL = "/don_confiao/api/sales/fill_codes/"
|
||||||
|
FILL_CATALOG_SALES_CODES_URL = "/don_confiao/api/catalog_sales/fill_codes/"
|
||||||
|
|
||||||
|
|
||||||
|
def get_credentials():
|
||||||
|
username = input("Usuario: ")
|
||||||
|
password = getpass.getpass("Contraseña: ")
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(domain, username, password):
|
||||||
|
url = domain.rstrip("/") + TOKEN_URL
|
||||||
|
response = requests.post(
|
||||||
|
url, json={"username": username, "password": password}
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(
|
||||||
|
f"Error al obtener token: {response.status_code} {response.text}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
return response.json()["access"]
|
||||||
|
|
||||||
|
|
||||||
|
def fill_codes(domain, token, url):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
full_url = domain.rstrip("/") + url
|
||||||
|
response = requests.post(full_url, headers=headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(
|
||||||
|
f"Error al llenar códigos ({url}): {response.status_code} {response.text}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Asigna un código único a todas las ventas y pedidos que lo requieran."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"domain", help="Dominio del backend (ej: http://localhost:8000)"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
username, password = get_credentials()
|
||||||
|
token = get_token(args.domain, username, password)
|
||||||
|
print("Token obtenido correctamente.")
|
||||||
|
|
||||||
|
print("Asignando códigos a ventas...")
|
||||||
|
sales_result = fill_codes(args.domain, token, FILL_SALES_CODES_URL)
|
||||||
|
print(f" [OK] Ventas actualizadas: {sales_result['sales_updated']}")
|
||||||
|
|
||||||
|
print("Asignando códigos a pedidos de catálogo...")
|
||||||
|
catalog_result = fill_codes(
|
||||||
|
args.domain, token, FILL_CATALOG_SALES_CODES_URL
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" [OK] Pedidos de catálogo actualizados: "
|
||||||
|
f"{catalog_result['catalog_sales_updated']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"\nResumen: {sales_result['sales_updated']} ventas, "
|
||||||
|
f"{catalog_result['catalog_sales_updated']} pedidos de catálogo actualizados"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -153,6 +153,7 @@ CATALOGUE_BACKGROUND_IMAGES_RGBA = os.environ.get(
|
|||||||
_rgba_parts = CATALOGUE_BACKGROUND_IMAGES_RGBA.split(",")
|
_rgba_parts = CATALOGUE_BACKGROUND_IMAGES_RGBA.split(",")
|
||||||
CATALOGUE_BACKGROUND_IMAGES_RGBA = tuple(int(p.strip()) for p in _rgba_parts)
|
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)))
|
CATALOGUE_MAX_UPLOAD_SIZE = int(os.environ.get("CATALOGUE_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024)))
|
||||||
|
LOGO_MAX_UPLOAD_SIZE = int(os.environ.get("LOGO_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024)))
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ from .models.sales import (
|
|||||||
)
|
)
|
||||||
from .models.products import Product, ProductCategory
|
from .models.products import Product, ProductCategory
|
||||||
from .models.payments import ReconciliationJar
|
from .models.payments import ReconciliationJar
|
||||||
|
from .models.store_settings import StoreSettings
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(StoreSettings)
|
||||||
|
class StoreSettingsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("id", "address", "latitude", "longitude", "updated_at")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Customer)
|
@admin.register(Customer)
|
||||||
@@ -31,8 +37,9 @@ class SaleAdmin(admin.ModelAdmin):
|
|||||||
"customer",
|
"customer",
|
||||||
"payment_method",
|
"payment_method",
|
||||||
"external_id",
|
"external_id",
|
||||||
|
"code",
|
||||||
)
|
)
|
||||||
search_fields = ("date", "customer__name", "payment_method", "external_id")
|
search_fields = ("date", "customer__name", "payment_method", "external_id", "code")
|
||||||
list_filter = ("date", "customer__name", "payment_method")
|
list_filter = ("date", "customer__name", "payment_method")
|
||||||
|
|
||||||
@admin.register(SaleLine)
|
@admin.register(SaleLine)
|
||||||
@@ -51,8 +58,9 @@ class CatalogSaleAdmin(admin.ModelAdmin):
|
|||||||
"customer_address",
|
"customer_address",
|
||||||
"pickup_method",
|
"pickup_method",
|
||||||
"external_id",
|
"external_id",
|
||||||
|
"code",
|
||||||
)
|
)
|
||||||
search_fields = ("date","customer_name", "customer_phone", "pickup_method","external_id")
|
search_fields = ("date","customer_name", "customer_phone", "pickup_method","external_id", "code")
|
||||||
list_filter = ("date", "customer_name", "pickup_method")
|
list_filter = ("date", "customer_name", "pickup_method")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .sales import (
|
|||||||
CatalogSaleView,
|
CatalogSaleView,
|
||||||
SaleSummary,
|
SaleSummary,
|
||||||
CatalogSaleSummary,
|
CatalogSaleSummary,
|
||||||
|
PublicSaleSummaryView,
|
||||||
SalesForTrytonView,
|
SalesForTrytonView,
|
||||||
SalesToTrytonView,
|
SalesToTrytonView,
|
||||||
CatalogSalesToTrytonView,
|
CatalogSalesToTrytonView,
|
||||||
@@ -18,6 +19,7 @@ from .payments import (
|
|||||||
Pagination,
|
Pagination,
|
||||||
)
|
)
|
||||||
from .admin import AdminCodeValidateView
|
from .admin import AdminCodeValidateView
|
||||||
|
from .store_settings import StoreSettingsView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Catalogue Images
|
# Catalogue Images
|
||||||
@@ -33,6 +35,7 @@ __all__ = [
|
|||||||
"CatalogSaleView",
|
"CatalogSaleView",
|
||||||
"SaleSummary",
|
"SaleSummary",
|
||||||
"CatalogSaleSummary",
|
"CatalogSaleSummary",
|
||||||
|
"PublicSaleSummaryView",
|
||||||
"SalesForTrytonView",
|
"SalesForTrytonView",
|
||||||
"SalesToTrytonView",
|
"SalesToTrytonView",
|
||||||
"CatalogSalesToTrytonView",
|
"CatalogSalesToTrytonView",
|
||||||
@@ -44,4 +47,6 @@ __all__ = [
|
|||||||
"Pagination",
|
"Pagination",
|
||||||
# Admin
|
# Admin
|
||||||
"AdminCodeValidateView",
|
"AdminCodeValidateView",
|
||||||
|
# Store Settings
|
||||||
|
"StoreSettingsView",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.decorators import action
|
||||||
import io
|
import io
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
from ..models.sales import Sale, SaleLine, CatalogSale
|
from ..models.sales import (
|
||||||
|
Sale,
|
||||||
|
SaleLine,
|
||||||
|
CatalogSale,
|
||||||
|
generate_order_code,
|
||||||
|
)
|
||||||
from ..models.customers import Customer
|
from ..models.customers import Customer
|
||||||
from ..models.products import Product
|
from ..models.products import Product
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
@@ -20,11 +26,27 @@ from ..services.tryton.client import get_tryton_client
|
|||||||
from ..views import sales_to_tryton_csv
|
from ..views import sales_to_tryton_csv
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_codes(Model):
|
||||||
|
rows = Model.objects.all()
|
||||||
|
for row in rows:
|
||||||
|
row.code = generate_order_code()
|
||||||
|
Model.objects.bulk_update(rows, ["code"])
|
||||||
|
return len(rows)
|
||||||
|
|
||||||
|
|
||||||
class SaleView(viewsets.ModelViewSet):
|
class SaleView(viewsets.ModelViewSet):
|
||||||
queryset = Sale.objects.all()
|
queryset = Sale.objects.all()
|
||||||
serializer_class = SaleSerializer
|
serializer_class = SaleSerializer
|
||||||
permission_classes = [IsNotPublico]
|
permission_classes = [IsNotPublico]
|
||||||
|
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["post"],
|
||||||
|
permission_classes=[IsAuthenticated, IsAdministrator],
|
||||||
|
)
|
||||||
|
def fill_codes(self, request):
|
||||||
|
return Response({"sales_updated": _fill_codes(Sale)})
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
customer = Customer.objects.get(pk=data["customer"])
|
customer = Customer.objects.get(pk=data["customer"])
|
||||||
@@ -60,7 +82,17 @@ class CatalogSaleView(viewsets.ModelViewSet):
|
|||||||
queryset = CatalogSale.objects.all()
|
queryset = CatalogSale.objects.all()
|
||||||
serializer_class = CatalogSaleSerializer
|
serializer_class = CatalogSaleSerializer
|
||||||
|
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["post"],
|
||||||
|
permission_classes=[IsAuthenticated, IsAdministrator],
|
||||||
|
)
|
||||||
|
def fill_codes(self, request):
|
||||||
|
return Response({"catalog_sales_updated": _fill_codes(CatalogSale)})
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
|
if self.action == "fill_codes":
|
||||||
|
return [IsAuthenticated(), IsAdministrator()]
|
||||||
if self.action == "create":
|
if self.action == "create":
|
||||||
return [IsAuthenticated()]
|
return [IsAuthenticated()]
|
||||||
return [IsNotPublico()]
|
return [IsNotPublico()]
|
||||||
@@ -71,7 +103,9 @@ class SaleSummary(APIView):
|
|||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
sale = Sale.objects.get(pk=id)
|
sale = Sale.objects.get(pk=id)
|
||||||
serializer = SaleSummarySerializer(sale)
|
serializer = SaleSummarySerializer(
|
||||||
|
sale, context={"request": request}
|
||||||
|
)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
@@ -80,10 +114,45 @@ class CatalogSaleSummary(APIView):
|
|||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
catalog_sale = CatalogSale.objects.get(pk=id)
|
catalog_sale = CatalogSale.objects.get(pk=id)
|
||||||
serializer = CatalogSaleSummarySerializer(catalog_sale)
|
serializer = CatalogSaleSummarySerializer(
|
||||||
|
catalog_sale, context={"request": request}
|
||||||
|
)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicSaleSummaryView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def get(self, request, code=None):
|
||||||
|
code = code or request.query_params.get("code")
|
||||||
|
if not code:
|
||||||
|
return Response(
|
||||||
|
{"detail": "code is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
sale = Sale.objects.filter(code=code).first()
|
||||||
|
if sale:
|
||||||
|
serializer = SaleSummarySerializer(
|
||||||
|
sale, context={"request": request}
|
||||||
|
)
|
||||||
|
data = serializer.data
|
||||||
|
data["type"] = "sale"
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
catalog_sale = CatalogSale.objects.filter(code=code).first()
|
||||||
|
if catalog_sale:
|
||||||
|
serializer = CatalogSaleSummarySerializer(
|
||||||
|
catalog_sale, context={"request": request}
|
||||||
|
)
|
||||||
|
data = serializer.data
|
||||||
|
data["type"] = "catalog"
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"detail": "order not found"}, status=404
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SalesForTrytonView(APIView):
|
class SalesForTrytonView(APIView):
|
||||||
permission_classes = [IsAuthenticated, IsAdministrator]
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
|||||||
32
tienda_ilusion/don_confiao/api/store_settings.py
Normal file
32
tienda_ilusion/don_confiao/api/store_settings.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from ..models.store_settings import StoreSettings
|
||||||
|
from ..permissions import IsAdministrator
|
||||||
|
from ..serializers.store_settings import StoreSettingsSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class StoreSettingsView(APIView):
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return [AllowAny()]
|
||||||
|
return [IsAuthenticated(), IsAdministrator()]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
settings = StoreSettings.get_singleton()
|
||||||
|
return Response(
|
||||||
|
StoreSettingsSerializer(
|
||||||
|
settings, context={"request": request}
|
||||||
|
).data
|
||||||
|
)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
settings = StoreSettings.get_singleton()
|
||||||
|
serializer = StoreSettingsSerializer(
|
||||||
|
settings, data=request.data, partial=True, context={"request": request}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-08 18:24
|
||||||
|
|
||||||
|
import don_confiao.models.sales
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0049_catalogueimage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='catalogsale',
|
||||||
|
name='code',
|
||||||
|
field=models.CharField(default=don_confiao.models.sales.generate_order_code, editable=False, max_length=32),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='code',
|
||||||
|
field=models.CharField(default=don_confiao.models.sales.generate_order_code, editable=False, max_length=32),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
tienda_ilusion/don_confiao/migrations/0050_storesettings.py
Normal file
27
tienda_ilusion/don_confiao/migrations/0050_storesettings.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-08 17:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0049_catalogueimage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StoreSettings',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('address', models.TextField()),
|
||||||
|
('latitude', models.FloatField(blank=True, null=True)),
|
||||||
|
('longitude', models.FloatField(blank=True, null=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Configuración de la tienda',
|
||||||
|
'verbose_name_plural': 'Configuración de la tienda',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-08 19:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0050_storesettings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='storesettings',
|
||||||
|
name='logo',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-10 03:51
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0050_catalogsale_code_sale_code'),
|
||||||
|
('don_confiao', '0051_storesettings_logo'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .store_settings import StoreSettings
|
||||||
|
|
||||||
|
__all__ = ["StoreSettings"]
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ from .products import Product
|
|||||||
from .payments import PaymentMethods, ReconciliationJar
|
from .payments import PaymentMethods, ReconciliationJar
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def generate_order_code():
|
||||||
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
class SaleAbstractModel(models.Model):
|
class SaleAbstractModel(models.Model):
|
||||||
@@ -11,6 +16,9 @@ class SaleAbstractModel(models.Model):
|
|||||||
date = models.DateTimeField("Date")
|
date = models.DateTimeField("Date")
|
||||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||||
description = models.CharField(max_length=255, null=True, blank=True)
|
description = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
code = models.CharField(
|
||||||
|
max_length=32, default=generate_order_code, editable=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|||||||
21
tienda_ilusion/don_confiao/models/store_settings.py
Normal file
21
tienda_ilusion/don_confiao/models/store_settings.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class StoreSettings(models.Model):
|
||||||
|
address = models.TextField()
|
||||||
|
latitude = models.FloatField(null=True, blank=True)
|
||||||
|
longitude = models.FloatField(null=True, blank=True)
|
||||||
|
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Configuración de la tienda"
|
||||||
|
verbose_name_plural = "Configuración de la tienda"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"StoreSettings (id={self.id})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_singleton(cls):
|
||||||
|
instance, _ = cls.objects.get_or_create(pk=1)
|
||||||
|
return instance
|
||||||
@@ -16,6 +16,7 @@ from .payments import (
|
|||||||
ReconciliationJarSerializer,
|
ReconciliationJarSerializer,
|
||||||
PaymentMethodSerializer,
|
PaymentMethodSerializer,
|
||||||
)
|
)
|
||||||
|
from .store_settings import StoreSettingsSerializer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Catalogue Images
|
# Catalogue Images
|
||||||
@@ -39,4 +40,6 @@ __all__ = [
|
|||||||
# Payments
|
# Payments
|
||||||
"ReconciliationJarSerializer",
|
"ReconciliationJarSerializer",
|
||||||
"PaymentMethodSerializer",
|
"PaymentMethodSerializer",
|
||||||
|
# Store Settings
|
||||||
|
"StoreSettingsSerializer",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from ..models.sales import (
|
from ..models.sales import (
|
||||||
Sale,
|
Sale,
|
||||||
@@ -11,6 +12,17 @@ from .products import ListProductSerializer
|
|||||||
from .customers import ListCustomerSerializer
|
from .customers import ListCustomerSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PublicSummaryLinkMixin:
|
||||||
|
def get_link(self, obj):
|
||||||
|
path = reverse(
|
||||||
|
"don_confiao:public_purchase_summary", args=[obj.code]
|
||||||
|
)
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request:
|
||||||
|
return request.build_absolute_uri(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
class SaleLineSerializer(serializers.ModelSerializer):
|
class SaleLineSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SaleLine
|
model = SaleLine
|
||||||
@@ -24,6 +36,7 @@ class SaleSerializer(serializers.ModelSerializer):
|
|||||||
model = Sale
|
model = Sale
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
|
"code",
|
||||||
"customer",
|
"customer",
|
||||||
"date",
|
"date",
|
||||||
"saleline_set",
|
"saleline_set",
|
||||||
@@ -56,6 +69,7 @@ class CatalogSaleSerializer(serializers.ModelSerializer):
|
|||||||
model = CatalogSale
|
model = CatalogSale
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
|
"code",
|
||||||
"customer",
|
"customer",
|
||||||
"date",
|
"date",
|
||||||
"catalogsaleline_set",
|
"catalogsaleline_set",
|
||||||
@@ -87,13 +101,14 @@ class SummarySaleLineSerializer(serializers.ModelSerializer):
|
|||||||
fields = ["product", "quantity", "unit_price", "description"]
|
fields = ["product", "quantity", "unit_price", "description"]
|
||||||
|
|
||||||
|
|
||||||
class SaleSummarySerializer(serializers.ModelSerializer):
|
class SaleSummarySerializer(PublicSummaryLinkMixin, serializers.ModelSerializer):
|
||||||
customer = ListCustomerSerializer()
|
customer = ListCustomerSerializer()
|
||||||
lines = SummarySaleLineSerializer(many=True, source="saleline_set")
|
lines = SummarySaleLineSerializer(many=True, source="saleline_set")
|
||||||
|
link = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Sale
|
model = Sale
|
||||||
fields = ["id", "date", "customer", "payment_method", "lines"]
|
fields = ["id", "code", "date", "customer", "payment_method", "lines", "link"]
|
||||||
|
|
||||||
|
|
||||||
class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
||||||
@@ -104,15 +119,18 @@ class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
|||||||
fields = ["product", "quantity", "unit_price", "description"]
|
fields = ["product", "quantity", "unit_price", "description"]
|
||||||
|
|
||||||
|
|
||||||
class CatalogSaleSummarySerializer(serializers.ModelSerializer):
|
class CatalogSaleSummarySerializer(
|
||||||
|
PublicSummaryLinkMixin, serializers.ModelSerializer
|
||||||
|
):
|
||||||
customer = ListCustomerSerializer()
|
customer = ListCustomerSerializer()
|
||||||
lines = CatalogSummarySaleLineSerializer(
|
lines = CatalogSummarySaleLineSerializer(
|
||||||
many=True, source="catalogsaleline_set"
|
many=True, source="catalogsaleline_set"
|
||||||
)
|
)
|
||||||
|
link = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CatalogSale
|
model = CatalogSale
|
||||||
fields = ["id", "date", "customer", "lines"]
|
fields = ["id", "code", "date", "customer", "lines", "link"]
|
||||||
|
|
||||||
|
|
||||||
class SaleForRenconciliationSerializer(serializers.Serializer):
|
class SaleForRenconciliationSerializer(serializers.Serializer):
|
||||||
|
|||||||
22
tienda_ilusion/don_confiao/serializers/store_settings.py
Normal file
22
tienda_ilusion/don_confiao/serializers/store_settings.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from ..models.store_settings import StoreSettings
|
||||||
|
|
||||||
|
|
||||||
|
class StoreSettingsSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = StoreSettings
|
||||||
|
fields = ["id", "address", "latitude", "longitude", "logo", "updated_at"]
|
||||||
|
read_only_fields = ["id", "updated_at"]
|
||||||
|
|
||||||
|
def validate_logo(self, value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
max_size = settings.LOGO_MAX_UPLOAD_SIZE
|
||||||
|
if value.size > max_size:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Logo size exceeds the maximum allowed size of "
|
||||||
|
f"{max_size // (1024 * 1024)}MB."
|
||||||
|
)
|
||||||
|
return value
|
||||||
@@ -43,20 +43,28 @@ class TrytonSale:
|
|||||||
"shipment_address": self.sale.customer.address_external_id,
|
"shipment_address": self.sale.customer.address_external_id,
|
||||||
"invoice_address": self.sale.customer.address_external_id,
|
"invoice_address": self.sale.customer.address_external_id,
|
||||||
"currency": TRYTON_COP_CURRENCY,
|
"currency": TRYTON_COP_CURRENCY,
|
||||||
"comment": self.sale.description or "",
|
"comment": self.build_tryton_comment(),
|
||||||
"description": "Metodo pago: " + str(self.sale.payment_method or ""),
|
"description": "Metodo pago: "
|
||||||
|
+ str(self.sale.payment_method or ""),
|
||||||
"party": self.sale.customer.external_id,
|
"party": self.sale.customer.external_id,
|
||||||
"reference": "don_confiao " + str(self.sale.id),
|
"reference": "don_confiao " + str(self.sale.id),
|
||||||
"sale_date": self._format_date(self.sale.date),
|
"sale_date": self._format_date(self.sale.date),
|
||||||
"lines": [
|
"lines": [
|
||||||
[
|
[
|
||||||
"create",
|
"create",
|
||||||
[TrytonLineSale(line).to_tryton() for line in self.lines],
|
[
|
||||||
|
TrytonLineSale(line).to_tryton()
|
||||||
|
for line in self.lines
|
||||||
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"self_pick_up": True,
|
"self_pick_up": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def build_tryton_comment(self):
|
||||||
|
"""Construye el comentario para la venta en Tryton"""
|
||||||
|
return self.sale.description or ""
|
||||||
|
|
||||||
|
|
||||||
class TrytonLineSale:
|
class TrytonLineSale:
|
||||||
"""Representa una línea de venta para exportación a Tryton"""
|
"""Representa una línea de venta para exportación a Tryton"""
|
||||||
@@ -98,20 +106,36 @@ class TrytonCatalogSale:
|
|||||||
"shipment_address": self.catalog_sale.customer.address_external_id,
|
"shipment_address": self.catalog_sale.customer.address_external_id,
|
||||||
"invoice_address": self.catalog_sale.customer.address_external_id,
|
"invoice_address": self.catalog_sale.customer.address_external_id,
|
||||||
"currency": TRYTON_COP_CURRENCY,
|
"currency": TRYTON_COP_CURRENCY,
|
||||||
"comment": self.catalog_sale.description or "",
|
"comment": self.build_tryton_comment(),
|
||||||
"description": "Venta de catálogo",
|
"description": "Venta de catálogo",
|
||||||
"party": self.catalog_sale.customer.external_id,
|
"party": self.catalog_sale.customer.external_id,
|
||||||
"reference": "don_confiao_catalog " + str(self.catalog_sale.id),
|
"reference": "don_confiao_catalog "
|
||||||
|
+ str(self.catalog_sale.id),
|
||||||
"sale_date": self._format_date(self.catalog_sale.date),
|
"sale_date": self._format_date(self.catalog_sale.date),
|
||||||
"lines": [
|
"lines": [
|
||||||
[
|
[
|
||||||
"create",
|
"create",
|
||||||
[TrytonCatalogSaleLine(line).to_tryton() for line in self.lines],
|
[
|
||||||
|
TrytonCatalogSaleLine(line).to_tryton()
|
||||||
|
for line in self.lines
|
||||||
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"self_pick_up": True,
|
"self_pick_up": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def build_tryton_comment(self):
|
||||||
|
"""Construye el comentario para la venta en Tryton"""
|
||||||
|
|
||||||
|
comment = f"Cliente: {self.catalog_sale.customer_name or ''}\n"
|
||||||
|
comment += f"Teléfono: {self.catalog_sale.customer_phone or ''}\n"
|
||||||
|
comment += (
|
||||||
|
f"Dirección: {self.catalog_sale.customer_address or ''}\n"
|
||||||
|
)
|
||||||
|
comment += f"Método de recogida: {self.catalog_sale.pickup_method or ''}\n"
|
||||||
|
|
||||||
|
return comment
|
||||||
|
|
||||||
|
|
||||||
class TrytonCatalogSaleLine:
|
class TrytonCatalogSaleLine:
|
||||||
"""Representa una línea de catalog sale para exportación a Tryton"""
|
"""Representa una línea de catalog sale para exportación a Tryton"""
|
||||||
@@ -125,8 +149,12 @@ class TrytonCatalogSaleLine:
|
|||||||
def to_tryton(self):
|
def to_tryton(self):
|
||||||
return {
|
return {
|
||||||
"product": self.catalog_sale_line.product.external_id,
|
"product": self.catalog_sale_line.product.external_id,
|
||||||
"quantity": self._format_decimal(self.catalog_sale_line.quantity),
|
"quantity": self._format_decimal(
|
||||||
|
self.catalog_sale_line.quantity
|
||||||
|
),
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"unit": self.catalog_sale_line.product.unit_external_id,
|
"unit": self.catalog_sale_line.product.unit_external_id,
|
||||||
"unit_price": self._format_decimal(self.catalog_sale_line.unit_price),
|
"unit_price": self._format_decimal(
|
||||||
|
self.catalog_sale_line.unit_price
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import json
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from ..models.sales import Sale, SaleLine, CatalogSale, CatalogSaleLine
|
||||||
|
from ..models.customers import Customer
|
||||||
|
from ..models.products import Product
|
||||||
|
from ..services.tryton.client import TrytonSale, TrytonCatalogSale
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestSendSalesToTryton(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",
|
||||||
|
)
|
||||||
|
SaleLine.objects.create(
|
||||||
|
product=self.product,
|
||||||
|
quantity=2,
|
||||||
|
unit_price=3000,
|
||||||
|
sale=self.sale,
|
||||||
|
)
|
||||||
|
SaleLine.objects.create(
|
||||||
|
product=self.product,
|
||||||
|
quantity=3,
|
||||||
|
unit_price=5000,
|
||||||
|
sale=self.sale,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.catalog_sale = CatalogSale.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
date="2024-09-02",
|
||||||
|
customer_name="Camilo",
|
||||||
|
customer_phone="3001234567",
|
||||||
|
customer_address="Calle 123",
|
||||||
|
pickup_method="Recogida en tienda",
|
||||||
|
)
|
||||||
|
CatalogSaleLine.objects.create(
|
||||||
|
catalog_sale=self.catalog_sale,
|
||||||
|
product=self.product,
|
||||||
|
quantity=2,
|
||||||
|
unit_price=3000,
|
||||||
|
)
|
||||||
|
CatalogSaleLine.objects.create(
|
||||||
|
catalog_sale=self.catalog_sale,
|
||||||
|
product=self.product,
|
||||||
|
quantity=3,
|
||||||
|
unit_price=5000,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("don_confiao.api.sales.get_tryton_client")
|
||||||
|
def test_send_sales_to_tryton(self, mock_get_tryton_client):
|
||||||
|
external_id = "23423"
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.call.return_value = [external_id]
|
||||||
|
mock_get_tryton_client.return_value = mock_client
|
||||||
|
|
||||||
|
url = "/don_confiao/api/enviar_ventas_a_tryton"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
content, {"successful": [self.sale.id], "failed": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_sale = Sale.objects.get(id=self.sale.id)
|
||||||
|
self.assertEqual(updated_sale.external_id, external_id)
|
||||||
|
|
||||||
|
mock_get_tryton_client.assert_called_once()
|
||||||
|
|
||||||
|
mock_client.call.assert_called_once()
|
||||||
|
call_args = mock_client.call.call_args
|
||||||
|
method, params = call_args[0]
|
||||||
|
|
||||||
|
self.assertEqual(method, "model.sale.sale.create")
|
||||||
|
|
||||||
|
tryton_context = params[1]
|
||||||
|
self.assertEqual(tryton_context, {"company": 1, "shops": [1]})
|
||||||
|
|
||||||
|
sale_params = params[0][0]
|
||||||
|
self.assertEqual(sale_params["company"], 1)
|
||||||
|
self.assertEqual(sale_params["shipment_address"], "307")
|
||||||
|
self.assertEqual(sale_params["invoice_address"], "307")
|
||||||
|
self.assertEqual(sale_params["currency"], 31)
|
||||||
|
self.assertEqual(sale_params["comment"], "un comentario")
|
||||||
|
self.assertEqual(sale_params["description"], "Metodo pago: CASH")
|
||||||
|
self.assertEqual(sale_params["party"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
sale_params["reference"], f"don_confiao {self.sale.id}"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sale_params["sale_date"],
|
||||||
|
{"__class__": "date", "year": 2024, "month": 9, "day": 2},
|
||||||
|
)
|
||||||
|
self.assertTrue(sale_params["self_pick_up"])
|
||||||
|
|
||||||
|
sale_tryton = TrytonSale(
|
||||||
|
self.sale, SaleLine.objects.filter(sale=self.sale)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sale_tryton.build_tryton_comment(), self.sale.description
|
||||||
|
)
|
||||||
|
|
||||||
|
lines_block = sale_params["lines"]
|
||||||
|
self.assertEqual(len(lines_block), 1)
|
||||||
|
self.assertEqual(lines_block[0][0], "create")
|
||||||
|
|
||||||
|
lines = lines_block[0][1]
|
||||||
|
self.assertEqual(len(lines), 2)
|
||||||
|
|
||||||
|
self.assertEqual(lines[0]["product"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[0]["quantity"],
|
||||||
|
{"__class__": "Decimal", "decimal": "2.00"},
|
||||||
|
)
|
||||||
|
self.assertEqual(lines[0]["type"], "line")
|
||||||
|
self.assertEqual(lines[0]["unit"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[0]["unit_price"],
|
||||||
|
{"__class__": "Decimal", "decimal": "3000.00"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(lines[1]["product"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[1]["quantity"],
|
||||||
|
{"__class__": "Decimal", "decimal": "3.00"},
|
||||||
|
)
|
||||||
|
self.assertEqual(lines[1]["type"], "line")
|
||||||
|
self.assertEqual(lines[1]["unit"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[1]["unit_price"],
|
||||||
|
{"__class__": "Decimal", "decimal": "5000.00"},
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("don_confiao.api.sales.get_tryton_client")
|
||||||
|
def test_send_catalog_sales_to_tryton(self, mock_get_tryton_client):
|
||||||
|
external_id = "23423"
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.call.return_value = [external_id]
|
||||||
|
mock_get_tryton_client.return_value = mock_client
|
||||||
|
|
||||||
|
url = "/don_confiao/api/enviar_catalog_sales_a_tryton"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
content,
|
||||||
|
{"successful": [self.catalog_sale.id], "failed": []},
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = CatalogSale.objects.get(id=self.catalog_sale.id)
|
||||||
|
self.assertEqual(updated.external_id, external_id)
|
||||||
|
|
||||||
|
mock_get_tryton_client.assert_called_once()
|
||||||
|
|
||||||
|
mock_client.call.assert_called_once()
|
||||||
|
call_args = mock_client.call.call_args
|
||||||
|
method, params = call_args[0]
|
||||||
|
|
||||||
|
self.assertEqual(method, "model.sale.sale.create")
|
||||||
|
|
||||||
|
tryton_context = params[1]
|
||||||
|
self.assertEqual(tryton_context, {"company": 1, "shops": [1]})
|
||||||
|
|
||||||
|
sale_params = params[0][0]
|
||||||
|
self.assertEqual(sale_params["company"], 1)
|
||||||
|
self.assertEqual(sale_params["shipment_address"], "307")
|
||||||
|
self.assertEqual(sale_params["invoice_address"], "307")
|
||||||
|
self.assertEqual(sale_params["currency"], 31)
|
||||||
|
self.assertEqual(sale_params["description"], "Venta de catálogo")
|
||||||
|
self.assertEqual(sale_params["party"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
sale_params["reference"],
|
||||||
|
f"don_confiao_catalog {self.catalog_sale.id}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sale_params["sale_date"],
|
||||||
|
{"__class__": "date", "year": 2024, "month": 9, "day": 2},
|
||||||
|
)
|
||||||
|
self.assertTrue(sale_params["self_pick_up"])
|
||||||
|
|
||||||
|
expected_comment = (
|
||||||
|
"Cliente: Camilo\n"
|
||||||
|
"Teléfono: 3001234567\n"
|
||||||
|
"Dirección: Calle 123\n"
|
||||||
|
"Método de recogida: Recogida en tienda\n"
|
||||||
|
)
|
||||||
|
self.assertEqual(sale_params["comment"], expected_comment)
|
||||||
|
|
||||||
|
catalog_tryton = TrytonCatalogSale(
|
||||||
|
self.catalog_sale,
|
||||||
|
CatalogSaleLine.objects.filter(
|
||||||
|
catalog_sale=self.catalog_sale
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
catalog_tryton.build_tryton_comment(), expected_comment
|
||||||
|
)
|
||||||
|
|
||||||
|
lines_block = sale_params["lines"]
|
||||||
|
self.assertEqual(len(lines_block), 1)
|
||||||
|
self.assertEqual(lines_block[0][0], "create")
|
||||||
|
|
||||||
|
lines = lines_block[0][1]
|
||||||
|
self.assertEqual(len(lines), 2)
|
||||||
|
|
||||||
|
self.assertEqual(lines[0]["product"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[0]["quantity"],
|
||||||
|
{"__class__": "Decimal", "decimal": "2.00"},
|
||||||
|
)
|
||||||
|
self.assertEqual(lines[0]["type"], "line")
|
||||||
|
self.assertEqual(lines[0]["unit"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[0]["unit_price"],
|
||||||
|
{"__class__": "Decimal", "decimal": "3000.00"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(lines[1]["product"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[1]["quantity"],
|
||||||
|
{"__class__": "Decimal", "decimal": "3.00"},
|
||||||
|
)
|
||||||
|
self.assertEqual(lines[1]["type"], "line")
|
||||||
|
self.assertEqual(lines[1]["unit"], "1")
|
||||||
|
self.assertEqual(
|
||||||
|
lines[1]["unit_price"],
|
||||||
|
{"__class__": "Decimal", "decimal": "5000.00"},
|
||||||
|
)
|
||||||
222
tienda_ilusion/don_confiao/tests/test_order_summary_public.py
Normal file
222
tienda_ilusion/don_confiao/tests/test_order_summary_public.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient, APITestCase
|
||||||
|
|
||||||
|
from ..admin import CatalogSaleAdmin, SaleAdmin
|
||||||
|
from ..models.customers import Customer
|
||||||
|
from ..models.products import Product
|
||||||
|
from ..models.sales import CatalogSale, Sale
|
||||||
|
from ..serializers import CatalogSaleSerializer, SaleSerializer
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestOrderSummaryPublic(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.product = Product.objects.create(
|
||||||
|
name="Panela", price=5000, measuring_unit="UNIT"
|
||||||
|
)
|
||||||
|
self.customer = Customer.objects.create(
|
||||||
|
name="Camilo", external_id="18"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_sale(self):
|
||||||
|
sale = Sale.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
date=datetime(2024, 9, 2, tzinfo=timezone.utc),
|
||||||
|
payment_method="CASH",
|
||||||
|
)
|
||||||
|
sale.saleline_set.create(
|
||||||
|
product=self.product, quantity=2, unit_price=3000
|
||||||
|
)
|
||||||
|
return sale
|
||||||
|
|
||||||
|
def _create_catalog_sale(self):
|
||||||
|
catalog_sale = CatalogSale.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
date=datetime(2024, 9, 2, tzinfo=timezone.utc),
|
||||||
|
customer_name="Camilo",
|
||||||
|
)
|
||||||
|
catalog_sale.catalogsaleline_set.create(
|
||||||
|
product=self.product, quantity=2, unit_price=3000
|
||||||
|
)
|
||||||
|
return catalog_sale
|
||||||
|
|
||||||
|
def test_sale_has_auto_generated_code(self):
|
||||||
|
sale = self._create_sale()
|
||||||
|
self.assertTrue(sale.code)
|
||||||
|
self.assertEqual(len(sale.code), 32)
|
||||||
|
int(sale.code, 16)
|
||||||
|
|
||||||
|
def test_sale_codes_are_distinct(self):
|
||||||
|
sale1 = self._create_sale()
|
||||||
|
sale2 = self._create_sale()
|
||||||
|
self.assertNotEqual(sale1.code, sale2.code)
|
||||||
|
|
||||||
|
def test_catalog_sale_has_auto_generated_code(self):
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
self.assertTrue(catalog_sale.code)
|
||||||
|
self.assertEqual(len(catalog_sale.code), 32)
|
||||||
|
int(catalog_sale.code, 16)
|
||||||
|
|
||||||
|
def test_public_summary_by_code_without_auth(self):
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["code"], sale.code)
|
||||||
|
self.assertIn("link", data)
|
||||||
|
self.assertTrue(
|
||||||
|
data["link"].endswith(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(data["type"], "sale")
|
||||||
|
self.assertIn("lines", data)
|
||||||
|
self.assertGreaterEqual(len(data["lines"]), 1)
|
||||||
|
|
||||||
|
def test_public_summary_invalid_code_returns_404(self):
|
||||||
|
response = self.client.get(
|
||||||
|
"/don_confiao/resumen_publico/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_public_summary_by_query_param(self):
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
"/don_confiao/resumen_publico", {"code": sale.code}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.json()["code"], sale.code)
|
||||||
|
|
||||||
|
def test_public_summary_for_catalog_sale(self):
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{catalog_sale.code}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["code"], catalog_sale.code)
|
||||||
|
self.assertEqual(data["type"], "catalog")
|
||||||
|
|
||||||
|
def test_public_summary_with_duplicate_codes_does_not_500(self):
|
||||||
|
sale1 = self._create_sale()
|
||||||
|
sale2 = self._create_sale()
|
||||||
|
shared = "a" * 32
|
||||||
|
Sale.objects.filter(pk__in=[sale1.pk, sale2.pk]).update(code=shared)
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{shared}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_existing_authenticated_summaries_include_code_and_link(self):
|
||||||
|
self.login()
|
||||||
|
sale = self._create_sale()
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_compra_json/{sale.id}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["code"], sale.code)
|
||||||
|
self.assertIn("link", data)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_compra_catalogo_json/{catalog_sale.id}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["code"], catalog_sale.code)
|
||||||
|
self.assertIn("link", data)
|
||||||
|
|
||||||
|
def test_fill_codes_sales_endpoint_rewrites_all_codes(self):
|
||||||
|
self.login()
|
||||||
|
sale1 = self._create_sale()
|
||||||
|
sale2 = self._create_sale()
|
||||||
|
old_codes = {sale1.code, sale2.code}
|
||||||
|
|
||||||
|
response = self.client.post("/don_confiao/api/sales/fill_codes/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.json()["sales_updated"], 2)
|
||||||
|
|
||||||
|
sale1.refresh_from_db()
|
||||||
|
sale2.refresh_from_db()
|
||||||
|
new_codes = {sale1.code, sale2.code}
|
||||||
|
self.assertNotIn(sale1.code, old_codes)
|
||||||
|
self.assertEqual(len(new_codes), 2)
|
||||||
|
|
||||||
|
def test_fill_codes_catalog_sales_endpoint_rewrites_all_codes(self):
|
||||||
|
self.login()
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
old_code = catalog_sale.code
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/don_confiao/api/catalog_sales/fill_codes/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.json()["catalog_sales_updated"], 1)
|
||||||
|
|
||||||
|
catalog_sale.refresh_from_db()
|
||||||
|
self.assertNotEqual(catalog_sale.code, old_code)
|
||||||
|
|
||||||
|
def test_fill_codes_endpoints_require_authentication(self):
|
||||||
|
response = self.client.post("/don_confiao/api/sales/fill_codes/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/don_confiao/api/catalog_sales/fill_codes/"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_401_UNAUTHORIZED
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fill_codes_endpoints_require_admin(self):
|
||||||
|
regular_user = User.objects.create_user(
|
||||||
|
username="regular", password="regularpass"
|
||||||
|
)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=regular_user)
|
||||||
|
|
||||||
|
response = client.post("/don_confiao/api/sales/fill_codes/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/don_confiao/api/catalog_sales/fill_codes/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_admin_list_display_includes_code(self):
|
||||||
|
self.assertIn("code", SaleAdmin.list_display)
|
||||||
|
self.assertIn("code", CatalogSaleAdmin.list_display)
|
||||||
|
|
||||||
|
def test_sale_serializer_includes_code(self):
|
||||||
|
sale = self._create_sale()
|
||||||
|
data = SaleSerializer(sale).data
|
||||||
|
self.assertIn("code", data)
|
||||||
|
self.assertEqual(data["code"], sale.code)
|
||||||
|
|
||||||
|
def test_catalog_sale_serializer_includes_code(self):
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
data = CatalogSaleSerializer(catalog_sale).data
|
||||||
|
self.assertIn("code", data)
|
||||||
|
self.assertEqual(data["code"], catalog_sale.code)
|
||||||
|
|
||||||
|
def test_sales_list_endpoint_includes_code(self):
|
||||||
|
self.login()
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get("/don_confiao/api/sales/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
item = next(i for i in response.json() if i["id"] == sale.id)
|
||||||
|
self.assertEqual(item["code"], sale.code)
|
||||||
|
|
||||||
|
def test_catalog_sales_list_endpoint_includes_code(self):
|
||||||
|
self.login()
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
response = self.client.get("/don_confiao/api/catalog_sales/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
item = next(i for i in response.json() if i["id"] == catalog_sale.id)
|
||||||
|
self.assertEqual(item["code"], catalog_sale.code)
|
||||||
246
tienda_ilusion/don_confiao/tests/test_store_settings.py
Normal file
246
tienda_ilusion/don_confiao/tests/test_store_settings.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import override_settings
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient, APITestCase
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from ..models.store_settings import StoreSettings
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
URL = "/don_confiao/api/store_settings"
|
||||||
|
|
||||||
|
PNG_1X1 = base64.b64decode(
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
||||||
|
"+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreSettingsModel(APITestCase, LoginMixin):
|
||||||
|
def test_singleton_returns_single_instance(self):
|
||||||
|
first = StoreSettings.get_singleton()
|
||||||
|
second = StoreSettings.get_singleton()
|
||||||
|
self.assertEqual(first.pk, second.pk)
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_singleton_created_without_address(self):
|
||||||
|
instance = StoreSettings.get_singleton()
|
||||||
|
self.assertEqual(instance.address, "")
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
instance = StoreSettings.get_singleton()
|
||||||
|
self.assertEqual(str(instance), f"StoreSettings (id={instance.id})")
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreSettingsAPIPermissions(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.url = URL
|
||||||
|
|
||||||
|
def test_get_unauthenticated(self):
|
||||||
|
client = APIClient()
|
||||||
|
response = client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_patch_unauthenticated(self):
|
||||||
|
client = APIClient()
|
||||||
|
response = client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Calle falsa 123"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_patch_non_admin(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username="regularuser",
|
||||||
|
email="regular@example.com",
|
||||||
|
password="regularpass",
|
||||||
|
)
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
client = APIClient()
|
||||||
|
client.credentials(
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||||
|
)
|
||||||
|
response = client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Calle falsa 123"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_patch_publico(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username="publico",
|
||||||
|
email="publico@example.com",
|
||||||
|
password="publicopass",
|
||||||
|
)
|
||||||
|
user.profile.user_type = "publico"
|
||||||
|
user.profile.save()
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
client = APIClient()
|
||||||
|
client.credentials(
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||||
|
)
|
||||||
|
response = client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Calle falsa 123"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreSettingsAPI(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
self.url = URL
|
||||||
|
|
||||||
|
def test_get_creates_singleton(self):
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 0)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 1)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("id", data)
|
||||||
|
self.assertIn("address", data)
|
||||||
|
self.assertIn("latitude", data)
|
||||||
|
self.assertIn("longitude", data)
|
||||||
|
self.assertIn("logo", data)
|
||||||
|
self.assertIn("updated_at", data)
|
||||||
|
|
||||||
|
def test_patch_updates_settings(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
"address": "Carrera 5 # 10-20, Bogotá",
|
||||||
|
"latitude": 4.6097,
|
||||||
|
"longitude": -74.0817,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["address"], "Carrera 5 # 10-20, Bogotá")
|
||||||
|
self.assertEqual(data["latitude"], 4.6097)
|
||||||
|
self.assertEqual(data["longitude"], -74.0817)
|
||||||
|
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertEqual(settings.address, "Carrera 5 # 10-20, Bogotá")
|
||||||
|
self.assertEqual(settings.latitude, 4.6097)
|
||||||
|
self.assertEqual(settings.longitude, -74.0817)
|
||||||
|
|
||||||
|
def test_patch_partial(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Solo dirección"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertEqual(settings.address, "Solo dirección")
|
||||||
|
self.assertIsNone(settings.latitude)
|
||||||
|
|
||||||
|
def test_patch_reuses_singleton(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Primera"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Segunda"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_patch_invalid_coordinates(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"latitude": "not-a-number"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_patch_upload_logo(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": logo},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("/media/logos/", data["logo"])
|
||||||
|
self.assertTrue(data["logo"].startswith("http"))
|
||||||
|
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertIsNotNone(settings.logo)
|
||||||
|
self.assertTrue(settings.logo.name.startswith("logos/logo"))
|
||||||
|
|
||||||
|
def test_get_returns_absolute_logo_url(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
self.client.patch(self.url, {"logo": logo}, format="multipart")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data["logo"].startswith("http"))
|
||||||
|
|
||||||
|
def test_patch_logo_non_image(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
fake = SimpleUploadedFile("logo.txt", b"not an image", content_type="text/plain")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": fake},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@override_settings(LOGO_MAX_UPLOAD_SIZE=10)
|
||||||
|
def test_patch_logo_too_large(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": logo},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertFalse(settings.logo)
|
||||||
|
|
||||||
|
def test_patch_clear_logo(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
self.client.patch(self.url, {"logo": logo}, format="multipart")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIsNone(data["logo"])
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertFalse(settings.logo)
|
||||||
|
|
||||||
|
def test_patch_upload_logo_keeps_other_fields(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
"address": "Avenida Siempre Viva 742",
|
||||||
|
"logo": logo,
|
||||||
|
},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertEqual(settings.address, "Avenida Siempre Viva 742")
|
||||||
|
self.assertIsNotNone(settings.logo)
|
||||||
@@ -16,6 +16,7 @@ from .api import (
|
|||||||
CatalogSaleView,
|
CatalogSaleView,
|
||||||
SaleSummary,
|
SaleSummary,
|
||||||
CatalogSaleSummary,
|
CatalogSaleSummary,
|
||||||
|
PublicSaleSummaryView,
|
||||||
SalesForTrytonView,
|
SalesForTrytonView,
|
||||||
SalesToTrytonView,
|
SalesToTrytonView,
|
||||||
CatalogSalesToTrytonView,
|
CatalogSalesToTrytonView,
|
||||||
@@ -26,6 +27,8 @@ from .api import (
|
|||||||
SalesForReconciliationView,
|
SalesForReconciliationView,
|
||||||
# Admin
|
# Admin
|
||||||
AdminCodeValidateView,
|
AdminCodeValidateView,
|
||||||
|
# Store Settings
|
||||||
|
StoreSettingsView,
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = "don_confiao"
|
app_name = "don_confiao"
|
||||||
@@ -58,6 +61,16 @@ urlpatterns = [
|
|||||||
CatalogSaleSummary.as_view(),
|
CatalogSaleSummary.as_view(),
|
||||||
name="catalog_purchase_json_summary",
|
name="catalog_purchase_json_summary",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"resumen_publico/<str:code>",
|
||||||
|
PublicSaleSummaryView.as_view(),
|
||||||
|
name="public_purchase_summary",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"resumen_publico",
|
||||||
|
PublicSaleSummaryView.as_view(),
|
||||||
|
name="public_purchase_summary_query",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"payment_methods/all/select_format",
|
"payment_methods/all/select_format",
|
||||||
PaymentMethodView.as_view(),
|
PaymentMethodView.as_view(),
|
||||||
@@ -95,4 +108,5 @@ urlpatterns = [
|
|||||||
AdminCodeValidateView.as_view(),
|
AdminCodeValidateView.as_view(),
|
||||||
),
|
),
|
||||||
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
||||||
|
path("api/store_settings", StoreSettingsView.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user