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 %} + + +Vous êtes déjà connecté en tant que {{ request.user.email }}.
+ Passer à la commande + {% else %} +Vous n'êtes pas connecté. Veuillez choisir une option :
+ + + +Ou connectez-vous si vous avez déjà un compte.
+ +Pas encore de compte ? Créez-en un maintenant
+ {% endif %} +| Produit | +Couleur | +Taille | +Quantité | +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 +Aucun élément dans la commande.
+ {% endif %} +