From 5bbb5c39b9fcbf3d3755c2f05981af636fb53c5a Mon Sep 17 00:00:00 2001 From: Pablo Moreno Date: Mon, 13 May 2024 01:25:56 +0200 Subject: [PATCH] feat: added product batches and provider models --- config/settings/base.py | 16 +-- shop/api/v1/routers.py | 4 + shop/api/v1/serializers.py | 81 +++++++++---- shop/api/v1/viewsets.py | 23 +++- shop/migrations/0003_provider_productbatch.py | 114 ++++++++++++++++++ shop/models.py | 58 ++++++++- users/tests/test_change_password.py | 8 +- users/tests/test_login.py | 2 +- 8 files changed, 262 insertions(+), 44 deletions(-) create mode 100644 shop/migrations/0003_provider_productbatch.py diff --git a/config/settings/base.py b/config/settings/base.py index d023469..1c27093 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -149,17 +149,6 @@ REST_FRAMEWORK = { "PAGE_SIZE": PAGE_SIZE, } -JWT_AUTH = { - "JWT_SECRET_KEY": SECRET_KEY, - "JWT_VERIFY": True, - "JWT_VERIFY_EXPIRATION": True, - "JWT_EXPIRATION_DELTA": datetime.timedelta(days=7), - "JWT_ALLOW_REFRESH": True, - "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=7), - "JWT_AUTH_HEADER_PREFIX": "JWT", - "JWT_AUTH_COOKIE": "jwt", -} - LOCALE_PATHS = [ BASE_DIR / "locale", ] @@ -196,10 +185,7 @@ SIMPLE_JWT = { "JSON_ENCODER": None, "JWK_URL": None, "LEEWAY": 0, - "AUTH_HEADER_TYPES": ( - "Bearer", - "JWT", - ), + "AUTH_HEADER_TYPES": ("Bearer",), "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", "USER_ID_FIELD": "id", "USER_ID_CLAIM": "user_id", diff --git a/shop/api/v1/routers.py b/shop/api/v1/routers.py index 3ac356e..d962b9c 100644 --- a/shop/api/v1/routers.py +++ b/shop/api/v1/routers.py @@ -6,6 +6,8 @@ from shop.api.v1.viewsets import ( ProductPriceViewSet, OrderViewSet, OrderLineViewSet, + ProviderViewSet, + ProductBatchViewSet, ) @@ -13,8 +15,10 @@ router = DefaultRouter() router.register("taxes", TaxViewSet) router.register("customers", CustomerViewSet) +router.register("providers", ProviderViewSet) router.register("products", ProductViewSet) router.register("product-prices", ProductPriceViewSet) +router.register("product-batches", ProductBatchViewSet) router.register("orders", OrderViewSet) router.register("orders/(?P[^/.]+)/lines", OrderLineViewSet) diff --git a/shop/api/v1/serializers.py b/shop/api/v1/serializers.py index 1ddcd16..cf2b1a8 100644 --- a/shop/api/v1/serializers.py +++ b/shop/api/v1/serializers.py @@ -1,28 +1,16 @@ from rest_framework import serializers from files.api.v1.serializers import FileUploadSerializer -from shop.models import Product, ProductPrice, Customer, Tax, OrderLine, Order - - -class ProductSerializer(serializers.ModelSerializer): - images = FileUploadSerializer(many=True, read_only=True) - price = serializers.SerializerMethodField() - - def get_price(self, product): - last_price = product.prices.last() - - return ProductPriceSerializer(last_price).data - - class Meta: - model = Product - fields = ( - "name", - "description", - "stock", - "is_digital_asset", - "images", - "price", - ) +from shop.models import ( + Product, + ProductPrice, + Customer, + Tax, + OrderLine, + Order, + ProductBatch, + Provider, +) class TaxSerializer(serializers.ModelSerializer): @@ -47,6 +35,39 @@ class ProductPriceSerializer(serializers.ModelSerializer): ) +class ProductSerializer(serializers.ModelSerializer): + images = FileUploadSerializer(many=True, read_only=True) + price = ProductPriceSerializer() + + def get_price(self, product): + last_price = product.prices.last() + + return ProductPriceSerializer(last_price).data + + class Meta: + model = Product + fields = ( + "name", + "description", + "stock", + "is_digital_asset", + "images", + "price", + ) + + +class ProductBatchSerializer(serializers.ModelSerializer): + class Meta: + model = ProductBatch + fields = ( + "code", + "product", + "quantity", + "creation_date", + "expiration_date", + ) + + class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer @@ -63,6 +84,22 @@ class CustomerSerializer(serializers.ModelSerializer): ) +class ProviderSerializer(serializers.ModelSerializer): + class Meta: + model = Provider + fields = ( + "vat_id", + "name", + "email", + "phone", + "address", + "city", + "state", + "country", + "zip", + ) + + class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order diff --git a/shop/api/v1/viewsets.py b/shop/api/v1/viewsets.py index be825bc..de44b33 100644 --- a/shop/api/v1/viewsets.py +++ b/shop/api/v1/viewsets.py @@ -7,8 +7,19 @@ from shop.api.v1.serializers import ( TaxSerializer, OrderSerializer, OrderLineSerializer, + ProviderSerializer, + ProductBatchSerializer, +) +from shop.models import ( + Product, + ProductPrice, + Customer, + Tax, + Order, + OrderLine, + Provider, + ProductBatch, ) -from shop.models import Product, ProductPrice, Customer, Tax, Order, OrderLine class ProductViewSet(ModelViewSet): @@ -21,11 +32,21 @@ class ProductPriceViewSet(ModelViewSet): queryset = ProductPrice.objects.all() +class ProductBatchViewSet(ModelViewSet): + serializer_class = ProductBatchSerializer + queryset = ProductBatch.objects.all() + + class CustomerViewSet(ModelViewSet): serializer_class = CustomerSerializer queryset = Customer.objects.all() +class ProviderViewSet(ModelViewSet): + serializer_class = ProviderSerializer + queryset = Provider.objects.all() + + class TaxViewSet(ModelViewSet): serializer_class = TaxSerializer queryset = Tax.objects.all() diff --git a/shop/migrations/0003_provider_productbatch.py b/shop/migrations/0003_provider_productbatch.py new file mode 100644 index 0000000..4f3f491 --- /dev/null +++ b/shop/migrations/0003_provider_productbatch.py @@ -0,0 +1,114 @@ +# Generated by Django 5.0.3 on 2024-05-12 23:18 + +import django.db.models.deletion +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("shop", "0002_remove_product_unit_order_status_product_images_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Provider", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "vat_id", + models.CharField( + max_length=32, + unique=True, + verbose_name="Documento de identidad", + ), + ), + ("name", models.CharField(max_length=64, verbose_name="Nombre")), + ( + "email", + models.EmailField( + max_length=254, verbose_name="E-mail de contacto" + ), + ), + ( + "phone", + models.CharField( + blank=True, + default="", + max_length=16, + null=True, + verbose_name="Teléfono de contacto", + ), + ), + ("address", models.CharField(max_length=255, verbose_name="Dirección")), + ("city", models.CharField(max_length=64, verbose_name="Ciudad")), + ("state", models.CharField(max_length=64, verbose_name="Región")), + ("country", models.CharField(max_length=64, verbose_name="País")), + ("zip", models.CharField(max_length=32, verbose_name="Código postal")), + ], + options={ + "verbose_name": "Proveedor", + "verbose_name_plural": "Proveedores", + }, + ), + migrations.CreateModel( + name="ProductBatch", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + db_index=True, max_length=32, unique=True, verbose_name="Código" + ), + ), + ( + "quantity", + models.DecimalField( + decimal_places=4, + default=Decimal("1"), + max_digits=13, + verbose_name="Cantidad", + ), + ), + ( + "creation_date", + models.DateTimeField( + auto_now_add=True, verbose_name="Fecha de creación" + ), + ), + ( + "expiration_date", + models.DateField( + blank=True, + null=True, + verbose_name="Fecha de caducidad / consumo preferente", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="shop.product", + verbose_name="Producto", + ), + ), + ], + ), + ] diff --git a/shop/models.py b/shop/models.py index 7ccb71c..ae1259e 100644 --- a/shop/models.py +++ b/shop/models.py @@ -60,7 +60,7 @@ class ProductPrice(models.Model): return f"{self.price} - {self.tax.code}" @property - def price_with_tax(self): + def price_with_tax(self) -> Decimal: tax_value = self.price * Decimal(self.tax.value / 100) return round(self.price + tax_value, 2) @@ -69,6 +69,30 @@ class ProductPrice(models.Model): verbose_name_plural = _("Precio de producto") +class ProductBatch(models.Model): + code = models.CharField( + max_length=32, unique=True, db_index=True, verbose_name=_("Código") + ) + product = models.ForeignKey( + "shop.Product", on_delete=models.PROTECT, verbose_name=_("Producto") + ) + quantity = models.DecimalField( + max_digits=13, + decimal_places=4, + default=Decimal("1"), + verbose_name=_("Cantidad"), + ) + creation_date = models.DateTimeField( + auto_now_add=True, verbose_name=_("Fecha de creación") + ) + expiration_date = models.DateField( + blank=True, null=True, verbose_name=_("Fecha de caducidad / consumo preferente") + ) + + def __str__(self): + return f"{self.code} - {self.product.name} - {self.quantity}" + + class Tax(models.Model): code = models.CharField( max_length=8, blank=False, unique=True, verbose_name=_("Código de impuesto") @@ -112,6 +136,38 @@ class Customer(models.Model): verbose_name_plural = _("Clientes") +class Provider(models.Model): + vat_id = models.CharField( + max_length=32, + blank=False, + unique=True, + verbose_name=_("Documento de identidad"), + ) + name = models.CharField(max_length=64, blank=False, verbose_name=_("Nombre")) + + email = models.EmailField(blank=False, verbose_name=_("E-mail de contacto")) + phone = models.CharField( + max_length=16, + blank=True, + null=True, + default="", + verbose_name=_("Teléfono de contacto"), + ) + + address = models.CharField(max_length=255, blank=False, verbose_name=_("Dirección")) + city = models.CharField(max_length=64, blank=False, verbose_name=_("Ciudad")) + state = models.CharField(max_length=64, blank=False, verbose_name=_("Región")) + country = models.CharField(max_length=64, blank=False, verbose_name=_("País")) + zip = models.CharField(max_length=32, blank=False, verbose_name=_("Código postal")) + + def __str__(self): + return f"{self.vat_id} - {self.name}" + + class Meta: + verbose_name = _("Proveedor") + verbose_name_plural = _("Proveedores") + + class OrderLine(models.Model): order = models.ForeignKey( "shop.Order", diff --git a/users/tests/test_change_password.py b/users/tests/test_change_password.py index 9600620..968edf2 100644 --- a/users/tests/test_change_password.py +++ b/users/tests/test_change_password.py @@ -27,7 +27,7 @@ class TestChangePassword(APITestCase): assert response.status_code == status.HTTP_200_OK jwt_token = response.data.get("access") - self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}") + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( "/api/v1/auth/change-password/", { @@ -50,7 +50,7 @@ class TestChangePassword(APITestCase): assert response.status_code == status.HTTP_200_OK jwt_token = response.data.get("access") - self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}") + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( "/api/v1/auth/change-password/", { @@ -73,7 +73,7 @@ class TestChangePassword(APITestCase): assert response.status_code == status.HTTP_200_OK jwt_token = response.data.get("access") - self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}") + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( "/api/v1/auth/change-password/", { @@ -96,7 +96,7 @@ class TestChangePassword(APITestCase): assert response.status_code == status.HTTP_200_OK jwt_token = response.data.get("access") - self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}") + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( "/api/v1/auth/change-password/", { diff --git a/users/tests/test_login.py b/users/tests/test_login.py index f38d3eb..14a69bc 100644 --- a/users/tests/test_login.py +++ b/users/tests/test_login.py @@ -82,7 +82,7 @@ class TestLogin(APITestCase): assert response.status_code == status.HTTP_200_OK jwt_token = response.data.get("access") - self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}") + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.get("/api/v1/auth/me/") assert response.status_code == status.HTTP_200_OK