diff --git a/config/api/v1/urls.py b/config/api/v1/urls.py index fdf8282..e99663a 100644 --- a/config/api/v1/urls.py +++ b/config/api/v1/urls.py @@ -9,4 +9,5 @@ urlpatterns = [ "swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" ), path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), + path("shop/", include("shop.api.v1.routers")), ] diff --git a/shop/admin.py b/shop/admin.py index d4cc21d..888aedb 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from shop.models import Product, ProductPrice, OrderLine, Order, Tax +from shop.models import Product, ProductPrice, OrderLine, Order, Tax, Customer from unfold.admin import ModelAdmin @@ -35,6 +35,15 @@ class TaxAdmin(ModelAdmin): ) +@admin.register(Customer) +class CustomerAdmin(ModelAdmin): + list_display = ( + "vat_id", + "first_name", + "last_name", + ) + + @admin.register(OrderLine) class OrderLineAdmin(ModelAdmin): autocomplete_fields = ("product",) diff --git a/shop/api/__init__.py b/shop/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/api/v1/__init__.py b/shop/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/api/v1/routers.py b/shop/api/v1/routers.py new file mode 100644 index 0000000..8e31c16 --- /dev/null +++ b/shop/api/v1/routers.py @@ -0,0 +1,22 @@ +from rest_framework.routers import DefaultRouter +from shop.api.v1.viewsets import ( + TaxViewSet, + CustomerViewSet, + ProductViewSet, + ProductPriceViewSet, + OrderViewSet, + OrderLineViewSet, +) + + +router = DefaultRouter() + +router.register("taxes", TaxViewSet) +router.register("customers", CustomerViewSet) +router.register("product", ProductViewSet) +router.register("product-prices", ProductPriceViewSet) +router.register("orders", OrderViewSet) +router.register("orders/(?P[^/.]+)/lines", OrderLineViewSet) + + +urlpatterns = router.urls diff --git a/shop/api/v1/serializers.py b/shop/api/v1/serializers.py new file mode 100644 index 0000000..65d5b87 --- /dev/null +++ b/shop/api/v1/serializers.py @@ -0,0 +1,99 @@ +from rest_framework import serializers +from shop.models import Product, ProductPrice, Customer, Tax, OrderLine, Order + + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = ( + "name", + "description", + "stock", + "unit", + ) + + +class ProductPriceSerializer(serializers.ModelSerializer): + class Meta: + model = ProductPrice + fields = ( + "price", + "date", + "product", + ) + + +class CustomerSerializer(serializers.ModelSerializer): + class Meta: + model = Customer + fields = ( + "vat_id", + "first_name", + "last_name", + "email", + "address", + "city", + "state", + "country", + "zip", + ) + + +class TaxSerializer(serializers.ModelSerializer): + class Meta: + model = Tax + fields = ( + "code", + "value", + ) + + +class OrderSerializer(serializers.ModelSerializer): + class Meta: + model = Order + fields = ( + "uuid", + "creation_date", + "last_modification_date", + "base_total", + "total", + "customer", + "billing_address", + "billing_city", + "billing_state", + "billing_country", + "billing_zip", + "shipping_address", + "shipping_city", + "shipping_state", + "shipping_country", + "shipping_zip", + "contact_phone", + ) + read_only_fields = ( + "uuid", + "base_total", + "total", + ) + + +class OrderLineSerializer(serializers.ModelSerializer): + class Meta: + model = OrderLine + fields = ( + "product", + "quantity", + "order", + "price", + "base_total", + "total", + "taxes", + "tax_value", + ) + read_only_fields = ( + "price", + "base_total", + "total", + "taxes", + "tax_value", + ) diff --git a/shop/api/v1/viewsets.py b/shop/api/v1/viewsets.py new file mode 100644 index 0000000..be825bc --- /dev/null +++ b/shop/api/v1/viewsets.py @@ -0,0 +1,45 @@ +from rest_framework.viewsets import ModelViewSet + +from shop.api.v1.serializers import ( + ProductSerializer, + ProductPriceSerializer, + CustomerSerializer, + TaxSerializer, + OrderSerializer, + OrderLineSerializer, +) +from shop.models import Product, ProductPrice, Customer, Tax, Order, OrderLine + + +class ProductViewSet(ModelViewSet): + serializer_class = ProductSerializer + queryset = Product.objects.all() + + +class ProductPriceViewSet(ModelViewSet): + serializer_class = ProductPriceSerializer + queryset = ProductPrice.objects.all() + + +class CustomerViewSet(ModelViewSet): + serializer_class = CustomerSerializer + queryset = Customer.objects.all() + + +class TaxViewSet(ModelViewSet): + serializer_class = TaxSerializer + queryset = Tax.objects.all() + + +class OrderViewSet(ModelViewSet): + serializer_class = OrderSerializer + queryset = Order.objects.all() + lookup_field = "uuid" + + +class OrderLineViewSet(ModelViewSet): + serializer_class = OrderLineSerializer + queryset = OrderLine.objects.all() + + def get_queryset(self): + return super().get_queryset().filter(order_id=self.kwargs.get("order_id")) diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py index a8fe408..b5770ff 100644 --- a/shop/migrations/0001_initial.py +++ b/shop/migrations/0001_initial.py @@ -1,7 +1,8 @@ -# Generated by Django 5.0.3 on 2024-03-24 18:23 +# Generated by Django 5.0.3 on 2024-03-24 22:50 import django.db.models.deletion import django.utils.timezone +import uuid from decimal import Decimal from django.db import migrations, models @@ -50,64 +51,6 @@ class Migration(migrations.Migration): "verbose_name_plural": "Clientes", }, ), - migrations.CreateModel( - name="OrderLine", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "quantity", - models.DecimalField( - decimal_places=4, - default=Decimal("1"), - max_digits=13, - verbose_name="Cantidad", - ), - ), - ( - "price", - models.DecimalField( - decimal_places=4, max_digits=13, verbose_name="Precio" - ), - ), - ( - "base_total", - models.DecimalField( - decimal_places=4, - default=Decimal("0"), - max_digits=13, - verbose_name="Total sin impuestos", - ), - ), - ( - "tax_value", - models.PositiveIntegerField(verbose_name="Valor de impuestos"), - ), - ( - "taxes", - models.DecimalField( - decimal_places=4, max_digits=13, verbose_name="Impuestos" - ), - ), - ( - "total", - models.DecimalField( - decimal_places=4, max_digits=13, verbose_name="Total" - ), - ), - ], - options={ - "verbose_name": "Línea de pedido", - "verbose_name_plural": "Líneas de pedido", - }, - ), migrations.CreateModel( name="Product", fields=[ @@ -196,6 +139,7 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("uuid", models.UUIDField(default=uuid.uuid4, verbose_name="UUID")), ( "creation_date", models.DateTimeField( @@ -288,24 +232,86 @@ class Migration(migrations.Migration): verbose_name="Cliente", ), ), - ( - "lines", - models.ManyToManyField(to="shop.orderline", verbose_name="Líneas"), - ), ], options={ "verbose_name": "Pedido", "verbose_name_plural": "Pedidos", }, ), - migrations.AddField( - model_name="orderline", - name="product", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="shop.product", - verbose_name="Producto", - ), + migrations.CreateModel( + name="OrderLine", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "quantity", + models.DecimalField( + decimal_places=4, + default=Decimal("1"), + max_digits=13, + verbose_name="Cantidad", + ), + ), + ( + "price", + models.DecimalField( + decimal_places=4, max_digits=13, verbose_name="Precio" + ), + ), + ( + "base_total", + models.DecimalField( + decimal_places=4, + default=Decimal("0"), + max_digits=13, + verbose_name="Total sin impuestos", + ), + ), + ( + "tax_value", + models.PositiveIntegerField(verbose_name="Valor de impuestos"), + ), + ( + "taxes", + models.DecimalField( + decimal_places=4, max_digits=13, verbose_name="Impuestos" + ), + ), + ( + "total", + models.DecimalField( + decimal_places=4, max_digits=13, verbose_name="Total" + ), + ), + ( + "order", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="lines", + to="shop.order", + verbose_name="Pedido", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="shop.product", + verbose_name="Producto", + ), + ), + ], + options={ + "verbose_name": "Línea de pedido", + "verbose_name_plural": "Líneas de pedido", + }, ), migrations.CreateModel( name="ProductPrice", diff --git a/shop/models.py b/shop/models.py index 58d058b..7134c31 100644 --- a/shop/models.py +++ b/shop/models.py @@ -1,4 +1,5 @@ from decimal import Decimal +from uuid import uuid4 from django.db import models from django.utils import timezone @@ -108,6 +109,12 @@ class Customer(models.Model): class OrderLine(models.Model): + order = models.ForeignKey( + "shop.Order", + on_delete=models.CASCADE, + related_name="lines", + verbose_name=_("Pedido"), + ) product = models.ForeignKey( "shop.Product", on_delete=models.CASCADE, @@ -163,7 +170,7 @@ class OrderLine(models.Model): class Order(models.Model): - lines = models.ManyToManyField("shop.OrderLine", verbose_name=_("Líneas")) + uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True) creation_date = models.DateTimeField( auto_now_add=True, verbose_name=_("Fecha de creación") ) diff --git a/shop/tests/test_shop_models.py b/shop/tests/test_shop_models.py index 88b5229..e4fe574 100644 --- a/shop/tests/test_shop_models.py +++ b/shop/tests/test_shop_models.py @@ -1,6 +1,6 @@ from decimal import Decimal from rest_framework.test import APITestCase as TestCase -from shop.models import Product, ProductPrice, OrderLine, Tax, Customer +from shop.models import Product, ProductPrice, Tax, Customer from shop.utils import create_order_line_for_product, create_order @@ -45,25 +45,8 @@ class ShopModelsTest(TestCase): country="Tatooine", ) - l1 = create_order_line_for_product( - self.potatoes, - quantity=Decimal("1.5"), - tax=self.tax, - ) - l2 = create_order_line_for_product( - self.gasoline, - quantity=Decimal("40"), - tax=self.tax, - ) - l3 = create_order_line_for_product( - self.usb_c, - quantity=Decimal("1.00"), - tax=self.tax, - ) - order = create_order( customer=self.customer, - lines=OrderLine.objects.all(), billing_address=self.customer.address, billing_city=self.customer.city, billing_state=self.customer.state, @@ -71,5 +54,25 @@ class ShopModelsTest(TestCase): billing_country=self.customer.country, ) + l1 = create_order_line_for_product( + self.potatoes, + quantity=Decimal("1.5"), + tax=self.tax, + order=order, + ) + l2 = create_order_line_for_product( + self.gasoline, + quantity=Decimal("40"), + tax=self.tax, + order=order, + ) + l3 = create_order_line_for_product( + self.usb_c, + quantity=Decimal("1.00"), + tax=self.tax, + order=order, + ) + order.calculate_total_from_lines() + assert order.total == l1.total + l2.total + l3.total assert order.base_total == l1.base_total + l2.base_total + l3.base_total diff --git a/shop/utils.py b/shop/utils.py index 1d9a545..418b940 100644 --- a/shop/utils.py +++ b/shop/utils.py @@ -5,12 +5,15 @@ from shop.models import OrderLine, Tax, Product, Order, Customer from django.db.models import QuerySet -def create_order_line_for_product(product: Product, quantity: Decimal, tax: Tax): +def create_order_line_for_product( + product: Product, quantity: Decimal, tax: Tax, order: Order +): price = product.prices.last().price base_total = round(price * quantity, 2) taxes = round(base_total * (tax.value / Decimal("100")), 2) return OrderLine.objects.create( + order=order, product=product, quantity=quantity, price=price, @@ -23,7 +26,6 @@ def create_order_line_for_product(product: Product, quantity: Decimal, tax: Tax) def create_order( customer: Customer, - lines: Iterable, billing_address: str, billing_city: str, billing_state: str, @@ -61,8 +63,4 @@ def create_order( shipping_zip=shipping_zip, ) - for line in lines: - order.lines.add(line) - - order.calculate_total_from_lines() return order diff --git a/tpv/api/__init__.py b/tpv/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tpv/api/v1/__init__.py b/tpv/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tpv/api/v1/routers.py b/tpv/api/v1/routers.py new file mode 100644 index 0000000..e69de29 diff --git a/tpv/api/v1/serializers.py b/tpv/api/v1/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/tpv/api/v1/viewsets.py b/tpv/api/v1/viewsets.py new file mode 100644 index 0000000..e69de29