add ordering shop

shop
Raz 8 months ago
parent ca9c37f2c8
commit 027ce2d9bc
  1. 1
      padelclub_backend/urls.py
  2. 27
      shop/admin.py
  3. 5
      shop/forms.py
  4. 22
      shop/migrations/0008_alter_product_options_and_more.py
  5. 38
      shop/migrations/0009_order_orderitem.py
  6. 21
      shop/migrations/0010_guestuser.py
  7. 19
      shop/migrations/0011_order_guest_user.py
  8. 43
      shop/models.py
  9. 3
      shop/templates/shop/cart.html
  10. 47
      shop/templates/shop/checkout.html
  11. 65
      shop/templates/shop/order_confirmation.html
  12. 3
      shop/urls.py
  13. 95
      shop/views.py
  14. 21
      tournaments/custom_views.py
  15. 6
      tournaments/urls.py

@ -20,6 +20,7 @@ urlpatterns = [
# path('roads/', include(router.urls)), # path('roads/', include(router.urls)),
path("", include("tournaments.urls")), path("", include("tournaments.urls")),
path('shop/', include('shop.urls')),
# path("crm/", include("crm.urls")), # path("crm/", include("crm.urls")),
path('roads/', include("api.urls")), path('roads/', include("api.urls")),
path('kingdom/', admin.site.urls), path('kingdom/', admin.site.urls),

@ -1,9 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import Product, Color, Size from .models import Product, Color, Size, Order, OrderItem, GuestUser
@admin.register(Product) @admin.register(Product)
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
list_display = ("title", "order", "price", "cut") list_display = ("title", "ordering_value", "price", "cut")
@admin.register(Color) @admin.register(Color)
class ColorAdmin(admin.ModelAdmin): class ColorAdmin(admin.ModelAdmin):
@ -12,3 +12,26 @@ class ColorAdmin(admin.ModelAdmin):
@admin.register(Size) @admin.register(Size)
class SizeAdmin(admin.ModelAdmin): class SizeAdmin(admin.ModelAdmin):
list_display = ("name",) 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]

@ -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)

@ -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',
),
]

@ -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')),
],
),
]

@ -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)),
],
),
]

@ -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'),
),
]

@ -2,6 +2,13 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.conf import settings 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): class ColorChoices(models.TextChoices):
RED = "Red", "Red" RED = "Red", "Red"
BLUE = "Blue", "Blue" BLUE = "Blue", "Blue"
@ -41,11 +48,11 @@ class Product(models.Model):
# Use string references to prevent circular imports # Use string references to prevent circular imports
colors = models.ManyToManyField("shop.Color", blank=True, related_name="products") colors = models.ManyToManyField("shop.Color", blank=True, related_name="products")
sizes = models.ManyToManyField("shop.Size", 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) cut = models.IntegerField(choices=CutChoices.choices, default=CutChoices.MEN)
class Meta: 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): def __str__(self):
return self.title return self.title
@ -64,3 +71,35 @@ class CartItem(models.Model):
def get_total_price(self): def get_total_price(self):
return self.product.price * self.quantity 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

@ -56,6 +56,9 @@
{% if cart_items %} {% if cart_items %}
<a class="cancel-nav-button" href="{% url 'shop:clear_cart' %}">Vider le panier</a> <a class="cancel-nav-button" href="{% url 'shop:clear_cart' %}">Vider le panier</a>
{% endif %} {% endif %}
<a class="confirm-nav-button" href="{% url 'shop:checkout' %}">Passer la commande</a>
</div> </div>
{% else %} {% else %}
<p>Votre panier est vide.</p> <p>Votre panier est vide.</p>

@ -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 %}
<nav class="margin10">
<a href="{% url 'shop:product_list' %}">La Boutique</a>
<a href="{% url 'index' %}" class="orange">Accueil</a>
<a href="{% url 'clubs' %}" class="orange">Clubs</a>
{% if user.is_authenticated %}
<a href="{% url 'my-tournaments' %}" class="orange">Mes tournois</a>
<a href="{% url 'profile' %}">Mon compte</a>
{% else %}
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
</nav>
<h1 class="club my-block topmargin20">Votre panier</h1>
<div class="grid-x">
<div class="small-12 medium-6 large-6 my-block">
<div class="bubble">
{% if request.user.is_authenticated %}
<p>Vous êtes déjà connecté en tant que {{ request.user.email }}.</p>
<a href="{% url 'shop:checkout' %}">Passer à la commande</a>
{% else %}
<p>Vous n'êtes pas connecté. Veuillez choisir une option :</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" name="guest_checkout">Continuer sans créer de compte</button>
</form>
<hr>
<p>Ou <a href="{% url 'login' %}?next={% url 'shop:checkout' %}">connectez-vous</a> si vous avez déjà un compte.</p>
<hr>
<p>Pas encore de compte ? <a href="{% url 'signup' %}?next={% url 'shop:checkout' %}">Créez-en un maintenant</a></p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

@ -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 %}
<nav class="margin10">
<a href="{% url 'shop:product_list' %}">La Boutique</a>
<a href="{% url 'index' %}" class="orange">Accueil</a>
<a href="{% url 'clubs' %}" class="orange">Clubs</a>
{% if user.is_authenticated %}
<a href="{% url 'my-tournaments' %}" class="orange">Mes tournois</a>
<a href="{% url 'profile' %}">Mon compte</a>
{% else %}
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
</nav>
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">Détails de la commande</h1>
<div class="bubble">
{% if order_items %}
<table class="cart-table">
<thead>
<tr>
<th>Produit</th>
<th>Couleur</th>
<th>Taille</th>
<th>Quantité</th>
<th class="price-column">Prix</th>
</tr>
</thead>
<tbody>
{% for item in order_items %}
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ item.product.title }}</td>
<td>{{ item.color.name }}</td>
<td>{{ item.size.name }}</td>
<td>{{ item.quantity }}</td>
<td class="price-column">{{ item.get_total_price }} €</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="total-label">Total:</td>
<td class="total-quantity">{{ order_items.total_quantity }}</td>
<td class="price-column total-price">{{ total }} €</td>
</tr>
</tfoot>
</table>
<div class="order-summary">
<p>Votre commande a été passée avec succès !</p>
<a class="back-to-shop-button" href="{% url 'shop:product_list' %}">Retour à la boutique</a>
</div>
{% else %}
<p>Aucun élément dans la commande.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

@ -12,5 +12,6 @@ urlpatterns = [
path('cart/update/<int:product_id>/', views.update_cart_view, name='update_cart'), path('cart/update/<int:product_id>/', views.update_cart_view, name='update_cart'),
path('cart/remove/<int:product_id>/', views.remove_from_cart_view, name='remove_from_cart'), path('cart/remove/<int:product_id>/', views.remove_from_cart_view, name='remove_from_cart'),
path('clear-cart/', views.clear_cart, name='clear_cart'), path('clear-cart/', views.clear_cart, name='clear_cart'),
path('checkout/', views.checkout, name='checkout'),
path('order/<int:order_id>/confirmation/', views.order_confirmation, name='order_confirmation'),
] ]

@ -1,8 +1,11 @@
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages from django.contrib import messages
from .models import Product, CartItem from .models import Product, Order, OrderItem, GuestUser
from django.db.models import Sum 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 from . import cart
@ -35,7 +38,7 @@ def add_to_cart_view(request, product_id):
color_id = request.POST.get('color') color_id = request.POST.get('color')
size_id = request.POST.get('size') 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') messages.success(request, f'{cart_item.quantity} x {product.title} added to your cart')
return redirect('shop:product_list') return redirect('shop:product_list')
@ -57,3 +60,89 @@ def clear_cart(request):
cart.clear_cart(request) cart.clear_cart(request)
messages.success(request, "Your cart has been cleared.") messages.success(request, "Your cart has been cleared.")
return redirect('shop:product_list') 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')

@ -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)

@ -5,6 +5,7 @@ from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm
from .custom_views import CustomLoginView
from . import views from . import views
@ -53,10 +54,7 @@ urlpatterns = [
path('terms-of-use/', views.terms_of_use, name='terms-of-use'), 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('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('mail-test/', views.simple_form_view, name='mail-test'), path('mail-test/', views.simple_form_view, name='mail-test'),
path('login/', auth_views.LoginView.as_view( path('login/', CustomLoginView.as_view(), name='login'),
template_name='registration/login.html',
authentication_form=EmailOrUsernameAuthenticationForm
), name='login'),
path('password_change/', path('password_change/',
auth_views.PasswordChangeView.as_view( auth_views.PasswordChangeView.as_view(
success_url='/profile/', # Redirect back to profile after success success_url='/profile/', # Redirect back to profile after success

Loading…
Cancel
Save