feat: cart detail leads to order creation

This commit is contained in:
2025-01-02 17:20:03 +01:00
parent ab2c9f4d22
commit 10b411ba33
9 changed files with 290 additions and 187 deletions
+50 -4
View File
@@ -1,10 +1,9 @@
# Generated by Django 5.1.3 on 2024-12-31 09:48 # Generated by Django 5.1.3 on 2025-01-02 13:36
import uuid
from decimal import Decimal
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import uuid
from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -894,4 +893,51 @@ class Migration(migrations.Migration):
"ordering": ("-date",), "ordering": ("-date",),
}, },
), ),
migrations.CreateModel(
name="WishlistedProduct",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"creation_date",
models.DateTimeField(
auto_now_add=True, verbose_name="fecha de creación"
),
),
(
"last_modification_date",
models.DateTimeField(
auto_now=True, verbose_name="fecha de última modificación"
),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="shop.product",
verbose_name="producto",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="usuario",
),
),
],
options={
"verbose_name": "productos deseados",
"verbose_name_plural": "productos deseados",
"unique_together": {("user", "product")},
},
),
] ]
-62
View File
@@ -1,62 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-02 09:35
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("shop", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="WishlistedProduct",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"creation_date",
models.DateTimeField(
auto_now_add=True, verbose_name="fecha de creación"
),
),
(
"last_modification_date",
models.DateTimeField(
auto_now=True, verbose_name="fecha de última modificación"
),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="shop.product",
verbose_name="producto",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="usuario",
),
),
],
options={
"verbose_name": "productos deseados",
"verbose_name_plural": "productos deseados",
},
),
]
+10 -7
View File
@@ -122,11 +122,7 @@ class Product(TimestampedModel):
def __str__(self): def __str__(self):
return self.name return self.name
def save( def save(self, *args, **kwargs):
self,
*args,
**kwargs,
):
if not self.pk: if not self.pk:
self.slug = slugify(self.name)[:128] self.slug = slugify(self.name)[:128]
super().save(*args, **kwargs) super().save(*args, **kwargs)
@@ -362,7 +358,7 @@ class Order(TimestampedModel):
user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name=_("cliente")) user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name=_("cliente"))
# Billing information # Datos de facturación
billing_address = models.CharField( billing_address = models.CharField(
max_length=255, blank=False, verbose_name=_("dirección de facturación") max_length=255, blank=False, verbose_name=_("dirección de facturación")
) )
@@ -379,7 +375,7 @@ class Order(TimestampedModel):
max_length=32, blank=False, verbose_name=_("código postal de facturación") max_length=32, blank=False, verbose_name=_("código postal de facturación")
) )
# Shipping information # Datos de envío
shipping_address = models.CharField( shipping_address = models.CharField(
max_length=255, blank=False, verbose_name=_("dirección") max_length=255, blank=False, verbose_name=_("dirección")
) )
@@ -499,6 +495,9 @@ class CustomerAddress(models.Model):
default = models.BooleanField(default=True, verbose_name=_("por defecto")) default = models.BooleanField(default=True, verbose_name=_("por defecto"))
hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto")) hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto"))
def __str__(self):
return f'{self.full_name} - {self.address}, {self.address_zip}, {self.address_state} - {self.address_country}'
class Meta: class Meta:
verbose_name = _("dirección de cliente") verbose_name = _("dirección de cliente")
verbose_name_plural = _("direcciones de cliente") verbose_name_plural = _("direcciones de cliente")
@@ -529,3 +528,7 @@ class WishlistedProduct(TimestampedModel):
class Meta: class Meta:
verbose_name = _("productos deseados") verbose_name = _("productos deseados")
verbose_name_plural = _("productos deseados") verbose_name_plural = _("productos deseados")
unique_together = (
"user",
"product",
)
+3 -2
View File
@@ -88,6 +88,7 @@ def create_order_from_cart(
shipping_state=shipping_address.address_state, shipping_state=shipping_address.address_state,
shipping_country=shipping_address.address_country, shipping_country=shipping_address.address_country,
shipping_zip=shipping_address.address_zip, shipping_zip=shipping_address.address_zip,
user=cart.user,
) )
for item in cart.items.all(): for item in cart.items.all():
@@ -95,7 +96,7 @@ def create_order_from_cart(
price: ProductPrice = product.price price: ProductPrice = product.price
base_total = price.price * item.quantity base_total = price.price * item.quantity
tax_value = price.tax.value tax_value = price.tax.value
taxes = base_total * (tax_value / 100) taxes = base_total * tax_value / Decimal(100)
total = base_total + taxes total = base_total + taxes
OrderLine.objects.create( OrderLine.objects.create(
@@ -119,7 +120,7 @@ def add_shipping_order_line(order, shipping_method):
shipping_price: ProductPrice = shipping_method.shipping_product.price shipping_price: ProductPrice = shipping_method.shipping_product.price
shipping_base_total = shipping_price.price shipping_base_total = shipping_price.price
shipping_tax_value = shipping_price.tax.value shipping_tax_value = shipping_price.tax.value
shipping_taxes = shipping_base_total * (shipping_tax_value / 100) shipping_taxes = shipping_base_total * (shipping_tax_value / Decimal(100))
shipping_total = shipping_base_total + shipping_taxes shipping_total = shipping_base_total + shipping_taxes
OrderLine.objects.create( OrderLine.objects.create(
+6
View File
@@ -11,3 +11,9 @@ class CartItemForm(forms.Form):
product = forms.IntegerField(widget=widgets.HiddenInput) product = forms.IntegerField(widget=widgets.HiddenInput)
quantity = forms.IntegerField() quantity = forms.IntegerField()
class CreateOrderForm(forms.Form):
shipping_address = forms.IntegerField()
billing_address = forms.IntegerField()
shipping_method = forms.IntegerField()
+62 -8
View File
@@ -1,5 +1,7 @@
{% load i18n %} {% load i18n %}
<section class="bg-white py-2 antialiased dark:bg-gray-900 md:py-8"> <section class="bg-white py-2 antialiased dark:bg-gray-900 md:py-8">
<form action="{% url 'web:cart_detail' %}" method="post">
{% csrf_token %}
<div class="mx-auto max-w-screen-xl px-4 2xl:px-0"> <div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">{% translate 'Carrito' %}</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">{% translate 'Carrito' %}</h2>
@@ -27,7 +29,8 @@
</span> </span>
</div> </div>
<div class="text-end md:order-4 md:w-32"> <div class="text-end md:order-4 md:w-32">
<p class="text-base font-bold text-gray-900 dark:text-white">{{ item.product.price.price_with_tax }} <p
class="text-base font-bold text-gray-900 dark:text-white">{{ item.product.price.price_with_tax }}
</p> </p>
</div> </div>
</div> </div>
@@ -59,13 +62,63 @@
{% endfor %} {% endfor %}
{% if items.count == 0 %} {% if items.count == 0 %}
<span class="text-base font-normal text-gray-500 dark:text-gray-400"> <p class="text-base font-normal text-gray-500 dark:text-gray-400">
{% translate 'No hay nada en el carrito...' %} {% translate 'No hay nada en el carrito...' %}
</span> </p>
<p>
<a href="{% url 'web:index' %}" title=""
class="inline-flex items-center gap-2 text-sm font-medium text-primary-700 underline hover:no-underline dark:text-primary-500">
{% translate 'Volver a inicio' %}
</a>
</p>
{% endif %} {% endif %}
</div>
<section>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">
{% translate 'Dirección de envío' %}
</h2>
{% for address in shipping_addresses %}
<input type="radio" id="shipping_address_{{ address.pk }}" name="shipping_address"
value="{{ address.pk }}">
<label for="shipping_address_{{ address.pk }}">
{{ address }}
</label>
{% endfor %}
</section>
<section>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">
{% translate 'Dirección de facturación' %}
</h2>
{% for address in billing_addresses %}
<input type="radio" id="billing_address_{{ address.pk }}" name="billing_address"
value="{{ address.pk }}">
<label for="billing_address_{{ address.pk }}">
{{ address }}
</label>
{% endfor %}
</section>
<section>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">
{% translate 'Opciones de envío' %}
</h2>
{% for shipping_method in shipping_methods %}
<input type="radio" id="shipping_method_{{ shipping_method.pk }}" name="shipping_method"
value="{{ shipping_method.pk }}">
<label for="shipping_method_{{ shipping_method.pk }}">
{{ shipping_method.name }} - {{ shipping_method.shipping_product.price.price_with_tax }}€
</label>
{% endfor %}
</section>
</div> </div>
</div>
<!-- Resumen del pedido -->
<div class="mx-auto mt-6 max-w-4xl flex-1 space-y-6 lg:mt-0 lg:w-full"> <div class="mx-auto mt-6 max-w-4xl flex-1 space-y-6 lg:mt-0 lg:w-full">
<div <div
class="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6"> class="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
@@ -74,7 +127,8 @@
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<dl class="flex items-center justify-between gap-4"> <dl class="flex items-center justify-between gap-4">
<dt class="text-base font-normal text-gray-500 dark:text-gray-400">{% translate 'Base imponible' %}</dt> <dt
class="text-base font-normal text-gray-500 dark:text-gray-400">{% translate 'Base imponible' %}</dt>
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ base_total }} €</dd> <dd class="text-base font-medium text-gray-900 dark:text-white">{{ base_total }} €</dd>
</dl> </dl>
@@ -101,12 +155,10 @@
</dl> </dl>
</div> </div>
<form action="">
<button <button
class="flex w-full items-center justify-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"> class="flex w-full items-center justify-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
{% translate 'Ir a pagar' %} {% translate 'Terminar pedido' %}
</button> </button>
</form>
<div class="flex items-center justify-center gap-2"> <div class="flex items-center justify-center gap-2">
<span class="text-sm font-normal text-gray-500 dark:text-gray-400"> {% translate 'o' %} </span> <span class="text-sm font-normal text-gray-500 dark:text-gray-400"> {% translate 'o' %} </span>
@@ -135,5 +187,7 @@
{# </div>#} {# </div>#}
</div> </div>
</div> </div>
</div> </div>
</form>
</section> </section>
@@ -74,7 +74,7 @@
class="mb-2 me-2 inline-flex w-full items-center justify-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm class="mb-2 me-2 inline-flex w-full items-center justify-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm
font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300
dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800" dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
role="button"> {% translate 'Ir a pagar' %} role="button"> {% translate 'Comprar' %}
</a> </a>
{% endif %} {% endif %}
+19 -1
View File
@@ -4,7 +4,14 @@ from django.shortcuts import get_object_or_404, render
from django.views.generic import TemplateView from django.views.generic import TemplateView
from shop.filters import ProductFilter from shop.filters import ProductFilter
from shop.models import CartItem, Product, ProductPrice, WishlistedProduct from shop.models import (
CartItem,
Product,
ProductPrice,
WishlistedProduct,
CustomerAddress,
ShippingMethod,
)
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
from web.utils import get_or_create_cart from web.utils import get_or_create_cart
@@ -84,6 +91,14 @@ def cart(request, *args, **kwargs):
total = base_total + tax_total total = base_total + tax_total
billing_addresses = CustomerAddress.objects.filter(
user=request.user, address_type=CustomerAddress.Types.BILLING
)
shipping_addresses = CustomerAddress.objects.filter(
user=request.user, address_type=CustomerAddress.Types.SHIPPING
)
shipping_methods = ShippingMethod.objects.all()
response = render( response = render(
request, request,
"components/cart/cart.html", "components/cart/cart.html",
@@ -93,6 +108,9 @@ def cart(request, *args, **kwargs):
"total": total, "total": total,
"base_total": base_total, "base_total": base_total,
"tax_total": tax_total, "tax_total": tax_total,
"billing_addresses": billing_addresses,
"shipping_addresses": shipping_addresses,
"shipping_methods": shipping_methods,
}, },
) )
+41 -4
View File
@@ -1,10 +1,19 @@
from django.http.response import HttpResponse, JsonResponse from django.http.response import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.text import gettext_lazy as _ from django.utils.text import gettext_lazy as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView from django.views.generic import TemplateView, CreateView
from shop.models import CartItem, Product, ProductCategory, WishlistedProduct from shop.models import (
CartItem,
Product,
ProductCategory,
WishlistedProduct,
CustomerAddress,
ShippingMethod,
)
from shop.utils import create_order_from_cart
from web.forms import CreateOrderForm
from web.models import WebSettings from web.models import WebSettings
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
from web.utils import get_or_create_cart from web.utils import get_or_create_cart
@@ -55,8 +64,9 @@ class ProductDetail(TemplateView):
} }
class CartDetail(TemplateView): class CartDetail(TemplateView, CreateView):
template_name = "web/cart_detail.html" template_name = "web/cart_detail.html"
form_class = CreateOrderForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
web_settings = WebSettings.load() web_settings = WebSettings.load()
@@ -70,6 +80,33 @@ class CartDetail(TemplateView):
"description": _("resumen del carrito"), "description": _("resumen del carrito"),
} }
def post(self, request, *args, **kwargs):
form = CreateOrderForm(request.POST)
if not form.is_valid():
context = self.get_context_data(**kwargs)
context.update({"errors": form.errors})
return self.render_to_response(context)
billing_address_id = form.cleaned_data.get("billing_address")
shipping_address_id = form.cleaned_data.get("shipping_address")
shipping_method_id = form.cleaned_data.get("shipping_method")
cart, created = get_or_create_cart(request)
billing_address = get_object_or_404(CustomerAddress, pk=billing_address_id)
shipping_address = get_object_or_404(CustomerAddress, pk=shipping_address_id)
shipping_method = get_object_or_404(ShippingMethod, pk=shipping_method_id)
order = create_order_from_cart(
cart=cart,
billing_address=billing_address,
shipping_address=shipping_address,
shipping_method=shipping_method,
)
return redirect(reverse("web:order", kwargs={"pk": order.pk}))
class WishlistView(TemplateView): class WishlistView(TemplateView):
template_name = "web/wishlist.html" template_name = "web/wishlist.html"