diff --git a/files/admin.py b/files/admin.py index 45f4570..8f94362 100644 --- a/files/admin.py +++ b/files/admin.py @@ -7,6 +7,6 @@ from files.models import FileUpload @admin.register(FileUpload) class FileUploadAdmin(admin.ModelAdmin): list_display = ( - 'id', - 'file', + "id", + "file", ) diff --git a/files/api/v1/serializers.py b/files/api/v1/serializers.py index dcdb8fb..1797c69 100644 --- a/files/api/v1/serializers.py +++ b/files/api/v1/serializers.py @@ -13,4 +13,4 @@ class FileUploadSerializer(serializers.ModelSerializer): class NoIDFileUploadSerializer(serializers.ModelSerializer): class Meta: model = FileUpload - fields = ("file", ) + fields = ("file",) diff --git a/shop/admin.py b/shop/admin.py index d56a76d..2be8cb5 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -1,5 +1,15 @@ from django.contrib import admin -from shop.models import Product, ProductPrice, OrderLine, Order, Tax, Customer, Provider, Tag, ProductBatch +from shop.models import ( + Product, + ProductPrice, + OrderLine, + Order, + Tax, + Customer, + Provider, + Tag, + ProductBatch, Brand, +) from unfold.admin import ModelAdmin @@ -76,6 +86,14 @@ class TagAdmin(ModelAdmin): ) +@admin.register(Brand) +class BrandAdmin(ModelAdmin): + list_display = ( + "id", + "name", + ) + + @admin.register(ProductBatch) class ProductBatchAdmin(ModelAdmin): list_display = ( @@ -84,4 +102,3 @@ class ProductBatchAdmin(ModelAdmin): "quantity", "provider", ) - diff --git a/shop/api/v1/filters.py b/shop/api/v1/filters.py index 7092fec..f2b3ae6 100644 --- a/shop/api/v1/filters.py +++ b/shop/api/v1/filters.py @@ -5,7 +5,7 @@ from shop.models import Product class ProductFilter(django_filters.FilterSet): - tags = django_filters.BaseInFilter(field_name='tags', label=_('Etiquetas')) + tags = django_filters.BaseInFilter(field_name="tags", label=_("Etiquetas")) class Meta: model = Product diff --git a/shop/api/v1/routers.py b/shop/api/v1/routers.py index 74d31a8..76ea448 100644 --- a/shop/api/v1/routers.py +++ b/shop/api/v1/routers.py @@ -9,6 +9,7 @@ from shop.api.v1.viewsets import ( ProviderViewSet, ProductBatchViewSet, TagViewSet, + BrandViewSet, ) @@ -19,6 +20,7 @@ router = DefaultRouter() router.register("taxes", TaxViewSet) router.register("tags", TagViewSet) +router.register("brands", BrandViewSet) router.register("customers", CustomerViewSet) router.register("providers", ProviderViewSet) router.register("products", ProductViewSet) diff --git a/shop/api/v1/serializers.py b/shop/api/v1/serializers.py index 7621d94..fc3962a 100644 --- a/shop/api/v1/serializers.py +++ b/shop/api/v1/serializers.py @@ -12,6 +12,7 @@ from shop.models import ( ProductBatch, Provider, Tag, + Brand, ) @@ -111,7 +112,7 @@ class CreateProductSerializer(serializers.ModelSerializer): url=validated_data.get("url"), ) - tags = validated_data.get('tags') + tags = validated_data.get("tags") for tag in tags: instance.tags.add(tag) @@ -201,6 +202,15 @@ class ProviderSerializer(serializers.ModelSerializer): ) +class BrandSerializer(serializers.ModelSerializer): + class Meta: + model = Brand + fields = ( + "id", + "name", + ) + + class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order @@ -250,5 +260,3 @@ class OrderLineSerializer(serializers.ModelSerializer): "taxes", "tax_value", ) - - diff --git a/shop/api/v1/viewsets.py b/shop/api/v1/viewsets.py index c1e91d8..c887cd8 100644 --- a/shop/api/v1/viewsets.py +++ b/shop/api/v1/viewsets.py @@ -22,7 +22,9 @@ from shop.api.v1.serializers import ( CreateProductSerializer, CreateProductPriceSerializer, TagSerializer, - ListProductSerializer, UpdateProductSerializer, + ListProductSerializer, + UpdateProductSerializer, + BrandSerializer, ) from shop.models import ( Product, @@ -34,6 +36,7 @@ from shop.models import ( Provider, ProductBatch, Tag, + Brand, ) from shop.api.v1.permissions import ProductPermissions, ProductPricePermissions @@ -41,12 +44,17 @@ from shop.api.v1.permissions import ProductPermissions, ProductPricePermissions class ProductViewSet(ModelViewSet): serializer_class = ProductSerializer - queryset = Product.objects.prefetch_related("prices").prefetch_related("tags").prefetch_related("images").all() + queryset = ( + Product.objects.prefetch_related("prices") + .prefetch_related("tags") + .prefetch_related("images") + .all() + ) permission_classes = ( IsAdminUser, ProductPermissions, ) - search_fields = ('name', ) + search_fields = ("name",) filterset_class = ProductFilter def get_serializer_class(self): @@ -54,7 +62,7 @@ class ProductViewSet(ModelViewSet): return CreateProductSerializer elif self.action == "list": return ListProductSerializer - elif self.action in ('update', 'partial_update'): + elif self.action in ("update", "partial_update"): return UpdateProductSerializer return self.serializer_class @@ -88,21 +96,25 @@ class ProductPriceViewSet(ModelViewSet): class ProductBatchViewSet(ModelViewSet): serializer_class = ProductBatchSerializer queryset = ProductBatch.objects.all() + permission_classes = (IsAdminUser,) class CustomerViewSet(ModelViewSet): serializer_class = CustomerSerializer queryset = Customer.objects.all() + permission_classes = (IsAdminUser,) class ProviderViewSet(ModelViewSet): serializer_class = ProviderSerializer queryset = Provider.objects.all() + permission_classes = (IsAdminUser,) class TaxViewSet(ModelViewSet): serializer_class = TaxSerializer queryset = Tax.objects.all() + permission_classes = (IsAdminUser,) class OrderViewSet(ModelViewSet): @@ -160,3 +172,10 @@ class OrderLineViewSet(ModelViewSet): class TagViewSet(ModelViewSet): serializer_class = TagSerializer queryset = Tag.objects.all() + permission_classes = (IsAdminUser,) + + +class BrandViewSet(ModelViewSet): + serializer_class = BrandSerializer + queryset = Brand.objects.all() + permission_classes = (IsAdminUser,) diff --git a/shop/migrations/0007_brand_alter_customer_options_alter_order_options_and_more.py b/shop/migrations/0007_brand_alter_customer_options_alter_order_options_and_more.py new file mode 100644 index 0000000..1ffbf88 --- /dev/null +++ b/shop/migrations/0007_brand_alter_customer_options_alter_order_options_and_more.py @@ -0,0 +1,86 @@ +# Generated by Django 5.0.6 on 2024-05-19 14:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("shop", "0006_alter_product_options_alter_productbatch_options_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Brand", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=100, unique=True, verbose_name="Nombre" + ), + ), + ], + options={ + "verbose_name": "marca", + "verbose_name_plural": "marcas", + }, + ), + migrations.AlterModelOptions( + name="customer", + options={"verbose_name": "cliente", "verbose_name_plural": "clientes"}, + ), + migrations.AlterModelOptions( + name="order", + options={"verbose_name": "pedido", "verbose_name_plural": "pedidos"}, + ), + migrations.AlterModelOptions( + name="orderline", + options={ + "verbose_name": "línea de pedido", + "verbose_name_plural": "líneas de pedido", + }, + ), + migrations.AlterModelOptions( + name="product", + options={ + "ordering": ("-id",), + "verbose_name": "producto", + "verbose_name_plural": "productos", + }, + ), + migrations.AlterModelOptions( + name="productbatch", + options={ + "verbose_name": "remesa de producto", + "verbose_name_plural": "remesas de productos", + }, + ), + migrations.AlterModelOptions( + name="productprice", + options={ + "verbose_name": "precio de producto", + "verbose_name_plural": "precio de producto", + }, + ), + migrations.AlterModelOptions( + name="provider", + options={"verbose_name": "proveedor", "verbose_name_plural": "proveedores"}, + ), + migrations.AlterModelOptions( + name="tag", + options={"verbose_name": "etiqueta", "verbose_name_plural": "etiquetas"}, + ), + migrations.AlterModelOptions( + name="tax", + options={"verbose_name": "impuesto", "verbose_name_plural": "impuestos"}, + ), + ] diff --git a/shop/migrations/0008_product_brand.py b/shop/migrations/0008_product_brand.py new file mode 100644 index 0000000..2fe5aca --- /dev/null +++ b/shop/migrations/0008_product_brand.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.6 on 2024-05-19 15:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("shop", "0007_brand_alter_customer_options_alter_order_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="product", + name="brand", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="shop.brand", + verbose_name="Marca", + ), + ), + ] diff --git a/shop/models.py b/shop/models.py index a4c50ab..97cd9d7 100644 --- a/shop/models.py +++ b/shop/models.py @@ -13,8 +13,8 @@ class Tag(models.Model): return self.name class Meta: - verbose_name = _("Etiqueta") - verbose_name_plural = _("Etiquetas") + verbose_name = _("etiqueta") + verbose_name_plural = _("etiquetas") class Product(models.Model): @@ -42,6 +42,13 @@ class Product(models.Model): "files.FileUpload", blank=True, verbose_name=_("Imágenes") ) tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("Etiquetas")) + brand = models.ForeignKey( + "shop.Brand", + blank=True, + null=True, + on_delete=models.SET_NULL, + verbose_name=_("Marca"), + ) def __str__(self): return self.name @@ -51,9 +58,9 @@ class Product(models.Model): return self.prices.last() class Meta: - verbose_name = _("Producto") - verbose_name_plural = _("Productos") - ordering = ('-id', ) + verbose_name = _("producto") + verbose_name_plural = _("productos") + ordering = ("-id",) class ProductPrice(models.Model): @@ -82,8 +89,8 @@ class ProductPrice(models.Model): return round(self.price + tax_value, 2) class Meta: - verbose_name = _("Precio de producto") - verbose_name_plural = _("Precio de producto") + verbose_name = _("precio de producto") + verbose_name_plural = _("precio de producto") class ProductBatch(models.Model): @@ -117,8 +124,8 @@ class ProductBatch(models.Model): return f"{self.code} - {self.product.name} - {self.quantity}" class Meta: - verbose_name = _("Remesa de producto") - verbose_name_plural = _("Remesas de productos") + verbose_name = _("remesa de producto") + verbose_name_plural = _("remesas de productos") class Tax(models.Model): @@ -133,8 +140,8 @@ class Tax(models.Model): return f"{self.code} - {self.value}%" class Meta: - verbose_name = _("Impuesto") - verbose_name_plural = _("Impuestos") + verbose_name = _("impuesto") + verbose_name_plural = _("impuestos") class Customer(models.Model): @@ -160,8 +167,8 @@ class Customer(models.Model): return f"{self.vat_id} - {self.first_name} {self.last_name}" class Meta: - verbose_name = _("Cliente") - verbose_name_plural = _("Clientes") + verbose_name = _("cliente") + verbose_name_plural = _("clientes") class Provider(models.Model): @@ -192,8 +199,18 @@ class Provider(models.Model): return f"{self.vat_id} - {self.name}" class Meta: - verbose_name = _("Proveedor") - verbose_name_plural = _("Proveedores") + verbose_name = _("proveedor") + verbose_name_plural = _("proveedores") + + +class Brand(models.Model): + name = models.CharField( + max_length=100, unique=True, blank=False, null=False, verbose_name=_("Nombre") + ) + + class Meta: + verbose_name = _("marca") + verbose_name_plural = _("marcas") class OrderLine(models.Model): @@ -253,8 +270,8 @@ class OrderLine(models.Model): return f"{self.product.name} - {self.quantity} - {self.price}" class Meta: - verbose_name = _("Línea de pedido") - verbose_name_plural = _("Líneas de pedido") + verbose_name = _("línea de pedido") + verbose_name_plural = _("líneas de pedido") class Order(models.Model): @@ -345,5 +362,5 @@ class Order(models.Model): self.save() class Meta: - verbose_name = _("Pedido") - verbose_name_plural = _("Pedidos") + verbose_name = _("pedido") + verbose_name_plural = _("pedidos") diff --git a/shop/tests/test_api_brands.py b/shop/tests/test_api_brands.py new file mode 100644 index 0000000..965143d --- /dev/null +++ b/shop/tests/test_api_brands.py @@ -0,0 +1,50 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from config.tests.mixins import TestUserAuthenticationMixin +from shop.models import Brand + + +class TestBrandsAPI(APITestCase, TestUserAuthenticationMixin): + model_name = "brand" + + def setUp(self): + self.create_user() + + def create_brands(self): + Brand.objects.create( + name="Marca", + ) + + def test_fetch_brands(self): + self.create_brands() + self.login() + response = self.client.get("/api/v1/shop/brands/") + assert response.status_code == status.HTTP_200_OK + assert len(response.data.get("results")) == 1 + + def test_create_retrieve_brand(self): + self.login() + response = self.client.post("/api/v1/shop/brands/", {"name": "Marca 1"}) + assert response.status_code == status.HTTP_201_CREATED + pk = response.data.get("id") + + response = self.client.get(f"/api/v1/shop/brands/{pk}/") + assert response.status_code == status.HTTP_200_OK + + def test_create_update_brand(self): + self.login() + response = self.client.post("/api/v1/shop/brands/", {"name": "Marca 1"}) + assert response.status_code == status.HTTP_201_CREATED + pk = response.data.get("id") + + response = self.client.get(f"/api/v1/shop/brands/{pk}/") + assert response.status_code == status.HTTP_200_OK + + response = self.client.patch(f"/api/v1/shop/brands/{pk}/", {"name": "Marca 2"}) + assert response.status_code == status.HTTP_200_OK + + response = self.client.get(f"/api/v1/shop/brands/{pk}/") + assert response.status_code == status.HTTP_200_OK + + assert response.data.get("name") == "Marca 2"