From 027ce2d9bcd9ef398345260002f16943470ad549 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 18 Mar 2025 19:28:46 +0100 Subject: [PATCH] add ordering shop --- padelclub_backend/urls.py | 1 + shop/admin.py | 27 +++++- shop/forms.py | 5 + .../0008_alter_product_options_and_more.py | 22 +++++ shop/migrations/0009_order_orderitem.py | 38 ++++++++ shop/migrations/0010_guestuser.py | 21 ++++ shop/migrations/0011_order_guest_user.py | 19 ++++ shop/models.py | 43 ++++++++- shop/templates/shop/cart.html | 3 + shop/templates/shop/checkout.html | 47 +++++++++ shop/templates/shop/order_confirmation.html | 65 +++++++++++++ shop/urls.py | 3 +- shop/views.py | 95 ++++++++++++++++++- tournaments/custom_views.py | 21 ++++ tournaments/urls.py | 6 +- 15 files changed, 404 insertions(+), 12 deletions(-) create mode 100644 shop/forms.py create mode 100644 shop/migrations/0008_alter_product_options_and_more.py create mode 100644 shop/migrations/0009_order_orderitem.py create mode 100644 shop/migrations/0010_guestuser.py create mode 100644 shop/migrations/0011_order_guest_user.py create mode 100644 shop/templates/shop/checkout.html create mode 100644 shop/templates/shop/order_confirmation.html create mode 100644 tournaments/custom_views.py diff --git a/padelclub_backend/urls.py b/padelclub_backend/urls.py index 202cd71..d406e44 100644 --- a/padelclub_backend/urls.py +++ b/padelclub_backend/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ # path('roads/', include(router.urls)), path("", include("tournaments.urls")), + path('shop/', include('shop.urls')), # path("crm/", include("crm.urls")), path('roads/', include("api.urls")), path('kingdom/', admin.site.urls), diff --git a/shop/admin.py b/shop/admin.py index 842c8fa..f4f6814 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -1,9 +1,9 @@ from django.contrib import admin -from .models import Product, Color, Size +from .models import Product, Color, Size, Order, OrderItem, GuestUser @admin.register(Product) class ProductAdmin(admin.ModelAdmin): - list_display = ("title", "order", "price", "cut") + list_display = ("title", "ordering_value", "price", "cut") @admin.register(Color) class ColorAdmin(admin.ModelAdmin): @@ -12,3 +12,26 @@ class ColorAdmin(admin.ModelAdmin): @admin.register(Size) class SizeAdmin(admin.ModelAdmin): list_display = ("name",) + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + readonly_fields = ('product', 'quantity', 'color', 'size', 'price') + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id', 'date_ordered', 'status', 'total_price') + inlines = [OrderItemInline] + +class GuestUserOrderInline(admin.TabularInline): + model = Order + extra = 0 + readonly_fields = ('date_ordered', 'total_price') + can_delete = False + show_change_link = True + exclude = ('user',) # Exclude the user field from the inline display + +@admin.register(GuestUser) +class GuestUserAdmin(admin.ModelAdmin): + list_display = ('email', 'phone') + inlines = [GuestUserOrderInline] diff --git a/shop/forms.py b/shop/forms.py new file mode 100644 index 0000000..c92b4db --- /dev/null +++ b/shop/forms.py @@ -0,0 +1,5 @@ +from django import forms + +class GuestCheckoutForm(forms.Form): + email = forms.EmailField(required=True) + phone = forms.CharField(max_length=20, required=True) diff --git a/shop/migrations/0008_alter_product_options_and_more.py b/shop/migrations/0008_alter_product_options_and_more.py new file mode 100644 index 0000000..513e9c5 --- /dev/null +++ b/shop/migrations/0008_alter_product_options_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.11 on 2025-03-18 14:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0007_product_cut'), + ] + + operations = [ + migrations.AlterModelOptions( + name='product', + options={'ordering': ['ordering_value', 'cut']}, + ), + migrations.RenameField( + model_name='product', + old_name='order', + new_name='ordering_value', + ), + ] diff --git a/shop/migrations/0009_order_orderitem.py b/shop/migrations/0009_order_orderitem.py new file mode 100644 index 0000000..c80df8f --- /dev/null +++ b/shop/migrations/0009_order_orderitem.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.11 on 2025-03-18 14:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('shop', '0008_alter_product_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_ordered', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('PAID', 'Paid'), ('SHIPPED', 'Shipped'), ('DELIVERED', 'Delivered'), ('CANCELED', 'Canceled')], default='PENDING', max_length=20)), + ('total_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('color', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.color')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shop.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product')), + ('size', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.size')), + ], + ), + ] diff --git a/shop/migrations/0010_guestuser.py b/shop/migrations/0010_guestuser.py new file mode 100644 index 0000000..441e980 --- /dev/null +++ b/shop/migrations/0010_guestuser.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.11 on 2025-03-18 14:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0009_order_orderitem'), + ] + + operations = [ + migrations.CreateModel( + name='GuestUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(max_length=20)), + ], + ), + ] diff --git a/shop/migrations/0011_order_guest_user.py b/shop/migrations/0011_order_guest_user.py new file mode 100644 index 0000000..41eeb73 --- /dev/null +++ b/shop/migrations/0011_order_guest_user.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.11 on 2025-03-18 17:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0010_guestuser'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='guest_user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shop.guestuser'), + ), + ] diff --git a/shop/models.py b/shop/models.py index f3c49ba..1d44ebb 100644 --- a/shop/models.py +++ b/shop/models.py @@ -2,6 +2,13 @@ from django.contrib.auth.models import User from django.db import models from django.conf import settings +class OrderStatus(models.TextChoices): + PENDING = 'PENDING', 'Pending' + PAID = 'PAID', 'Paid' + SHIPPED = 'SHIPPED', 'Shipped' + DELIVERED = 'DELIVERED', 'Delivered' + CANCELED = 'CANCELED', 'Canceled' + class ColorChoices(models.TextChoices): RED = "Red", "Red" BLUE = "Blue", "Blue" @@ -41,11 +48,11 @@ class Product(models.Model): # Use string references to prevent circular imports colors = models.ManyToManyField("shop.Color", blank=True, related_name="products") sizes = models.ManyToManyField("shop.Size", blank=True, related_name="products") - order = models.IntegerField(default=0, blank=False) + ordering_value = models.IntegerField(default=0, blank=False) cut = models.IntegerField(choices=CutChoices.choices, default=CutChoices.MEN) class Meta: - ordering = ['order', 'cut'] # Add this line to sort by title + ordering = ['ordering_value', 'cut'] # Add this line to sort by title def __str__(self): return self.title @@ -64,3 +71,35 @@ class CartItem(models.Model): def get_total_price(self): return self.product.price * self.quantity + +class GuestUser(models.Model): + email = models.EmailField() + phone = models.CharField(max_length=20) + + def __str__(self): + return f"{self.email}" + + +class Order(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) + date_ordered = models.DateTimeField(auto_now_add=True) + status = models.CharField(max_length=20, choices=OrderStatus.choices, default=OrderStatus.PENDING) + total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) + guest_user = models.ForeignKey(GuestUser, on_delete=models.CASCADE, null=True, blank=True) + + def __str__(self): + return f"Order #{self.id} - {self.status}" + +class OrderItem(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + color = models.ForeignKey(Color, on_delete=models.SET_NULL, null=True, blank=True) + size = models.ForeignKey(Size, on_delete=models.SET_NULL, null=True, blank=True) + price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity} x {self.product.title}" + + def get_total_price(self): + return self.price * self.quantity diff --git a/shop/templates/shop/cart.html b/shop/templates/shop/cart.html index 2c63185..9db4b55 100644 --- a/shop/templates/shop/cart.html +++ b/shop/templates/shop/cart.html @@ -56,6 +56,9 @@ {% if cart_items %} Vider le panier {% endif %} + + Passer la commande + {% else %}

Votre panier est vide.

diff --git a/shop/templates/shop/checkout.html b/shop/templates/shop/checkout.html new file mode 100644 index 0000000..97a1ad7 --- /dev/null +++ b/shop/templates/shop/checkout.html @@ -0,0 +1,47 @@ +{% extends 'tournaments/base.html' %} + +{% block head_title %}La boutique{% endblock %} +{% block first_title %}La boutique Padel Club{% endblock %} +{% block second_title %}Validation de la commande{% endblock %} + +{% block content %} + + +

Votre panier

+
+
+
+ {% if request.user.is_authenticated %} +

Vous êtes déjà connecté en tant que {{ request.user.email }}.

+ Passer à la commande + {% else %} +

Vous n'êtes pas connecté. Veuillez choisir une option :

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +
+ +

Ou connectez-vous si vous avez déjà un compte.

+ +
+ +

Pas encore de compte ? Créez-en un maintenant

+ {% endif %} +
+
+
+{% endblock %} diff --git a/shop/templates/shop/order_confirmation.html b/shop/templates/shop/order_confirmation.html new file mode 100644 index 0000000..d410492 --- /dev/null +++ b/shop/templates/shop/order_confirmation.html @@ -0,0 +1,65 @@ +{% extends 'tournaments/base.html' %} + +{% block head_title %}Confirmation de commande{% endblock %} +{% block first_title %}Confirmation de commande{% endblock %} +{% block second_title %}Merci pour votre commande !{% endblock %} + +{% block content %} + + +
+
+

Détails de la commande

+
+ {% if order_items %} + + + + + + + + + + + + {% for item in order_items %} + + + + + + + + {% endfor %} + + + + + + + + +
ProduitCouleurTailleQuantitéPrix
{{ item.product.title }}{{ item.color.name }}{{ item.size.name }}{{ item.quantity }}{{ item.get_total_price }} €
Total:{{ order_items.total_quantity }}{{ total }} €
+ +
+

Votre commande a été passée avec succès !

+ Retour à la boutique +
+ {% else %} +

Aucun élément dans la commande.

+ {% endif %} +
+
+
+{% endblock %} diff --git a/shop/urls.py b/shop/urls.py index 8458d6a..6a4d43d 100644 --- a/shop/urls.py +++ b/shop/urls.py @@ -12,5 +12,6 @@ urlpatterns = [ path('cart/update//', views.update_cart_view, name='update_cart'), path('cart/remove//', views.remove_from_cart_view, name='remove_from_cart'), path('clear-cart/', views.clear_cart, name='clear_cart'), - + path('checkout/', views.checkout, name='checkout'), + path('order//confirmation/', views.order_confirmation, name='order_confirmation'), ] diff --git a/shop/views.py b/shop/views.py index fada206..ffc20ea 100644 --- a/shop/views.py +++ b/shop/views.py @@ -1,8 +1,11 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages -from .models import Product, CartItem +from .models import Product, Order, OrderItem, GuestUser from django.db.models import Sum -from .cart import add_to_cart +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth import login +from django.contrib.auth.decorators import login_required +from .forms import GuestCheckoutForm from . import cart @@ -35,7 +38,7 @@ def add_to_cart_view(request, product_id): color_id = request.POST.get('color') size_id = request.POST.get('size') - cart_item = add_to_cart(request, product_id, quantity, color_id, size_id) + cart_item = cart.add_to_cart(request, product_id, quantity, color_id, size_id) messages.success(request, f'{cart_item.quantity} x {product.title} added to your cart') return redirect('shop:product_list') @@ -57,3 +60,89 @@ def clear_cart(request): cart.clear_cart(request) messages.success(request, "Your cart has been cleared.") return redirect('shop:product_list') + +def create_order(request): + cart_items = cart.get_cart_items(request) + total_price = sum(item.get_total_price() for item in cart_items) + + if request.user.is_authenticated: + # L'utilisateur est authentifié, créer la commande avec l'utilisateur + order = Order.objects.create( + user=request.user, + total_price=total_price + ) + else: + # L'utilisateur n'est pas authentifié, créer la commande avec l'utilisateur invité + try: + guest_user = GuestUser.objects.get(email=request.session['guest_email']) + order = Order.objects.create( + guest_user=guest_user, + total_price=total_price + ) + except (KeyError, GuestUser.DoesNotExist): + # Si l'utilisateur invité n'existe pas, créer une commande sans utilisateur + order = Order.objects.create( + total_price=total_price + ) + + + for cart_item in cart_items: + OrderItem.objects.create( + order=order, + product=cart_item.product, + quantity=cart_item.quantity, + color=cart_item.color, + size=cart_item.size, + price=cart_item.product.price + ) + + # Clear the cart after creating the order + cart.clear_cart(request) + + return order + +def order_confirmation(request, order_id): + order = get_object_or_404(Order, id=order_id) + order_items = order.items.all() + total = sum(item.get_total_price() for item in order_items) + return render(request, 'shop/order_confirmation.html', { + 'order': order, + 'order_items': order_items, + 'total': total + }) + +def checkout(request): + if request.user.is_authenticated: + # Créer la commande pour l'utilisateur authentifié + order = create_order(request) + + # Rediriger vers la confirmation de commande + return redirect('shop:order_confirmation', order.id) + + if request.method == 'GET': + form = GuestCheckoutForm() + return render(request, 'shop/checkout.html', {'form': form}) + + elif request.method == 'POST': + # Gérer la soumission du formulaire ici + form = GuestCheckoutForm(request.POST) + if form.is_valid(): + # Créer ou récupérer l'utilisateur invité + email = form.cleaned_data['email'] + phone = form.cleaned_data['phone'] + guest_user, created = GuestUser.objects.get_or_create(email=email, defaults={'phone': phone}) + + # Stocker l'e-mail de l'utilisateur invité dans la session + request.session['guest_email'] = email + + # Simuler le processus de paiement + # ... + + # Créer la commande + order = create_order(request) + + # Rediriger vers la confirmation de commande + return redirect('shop:order_confirmation', order.id) + + # Gérer les autres méthodes (par exemple, PUT, DELETE) si nécessaire + return redirect('shop:product_list') diff --git a/tournaments/custom_views.py b/tournaments/custom_views.py new file mode 100644 index 0000000..76c926a --- /dev/null +++ b/tournaments/custom_views.py @@ -0,0 +1,21 @@ +from django.contrib import messages +from django.contrib.auth import views as auth_views +from django.urls import reverse +from .forms import EmailOrUsernameAuthenticationForm + +class CustomLoginView(auth_views.LoginView): + template_name = 'registration/login.html' + authentication_form = EmailOrUsernameAuthenticationForm + print("CustomLoginView") + + def get_success_url(self): + next_url = self.request.POST.get('next') + print("CustomLoginView", "next_url", next_url, self.request.GET) + if next_url: + return next_url + else: + return reverse('index') + + def get(self, request, *args, **kwargs): + messages.get_messages(request).used = True + return super().get(request, *args, **kwargs) diff --git a/tournaments/urls.py b/tournaments/urls.py index b040d5a..402fbed 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -5,6 +5,7 @@ from django.conf import settings from django.conf.urls.static import static from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm +from .custom_views import CustomLoginView from . import views @@ -53,10 +54,7 @@ urlpatterns = [ path('terms-of-use/', views.terms_of_use, name='terms-of-use'), path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('mail-test/', views.simple_form_view, name='mail-test'), - path('login/', auth_views.LoginView.as_view( - template_name='registration/login.html', - authentication_form=EmailOrUsernameAuthenticationForm - ), name='login'), + path('login/', CustomLoginView.as_view(), name='login'), path('password_change/', auth_views.PasswordChangeView.as_view( success_url='/profile/', # Redirect back to profile after success