add shipping adress and refund option

sync_v2
Raz 6 months ago
parent e3e6603d65
commit cd71834fdf
  1. 94
      shop/admin.py
  2. 13
      shop/forms.py
  3. 41
      shop/migrations/0027_shippingaddress_alter_order_payment_status_and_more.py
  4. 19
      shop/models.py
  5. 226
      shop/static/shop/css/shop.css
  6. 60
      shop/stripe_utils.py
  7. 52
      shop/templates/admin/shop/order/preparation_view.html
  8. 55
      shop/templates/shop/cart.html
  9. 15
      shop/templates/shop/checkout.html
  10. 81
      shop/templates/shop/my_orders.html
  11. 150
      shop/templates/shop/order_detail.html
  12. 9
      shop/templates/shop/partials/navigation_base.html
  13. 124
      shop/templates/shop/partials/order_items_display.html
  14. 15
      shop/templates/shop/payment.html
  15. 14
      shop/templates/shop/payment_cancel.html
  16. 14
      shop/templates/shop/payment_success.html
  17. 14
      shop/templates/shop/product_list.html
  18. 6
      shop/urls.py
  19. 263
      shop/views.py
  20. 18
      tournaments/migrations/0119_alter_tournament_animation_type.py
  21. 36
      tournaments/templates/profile.html
  22. 1
      tournaments/templates/tournaments/navigation_base.html

@ -1,7 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.shortcuts import render from django.shortcuts import render
from .models import Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage, OrderStatus from .models import Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage, OrderStatus, ShippingAddress
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import path
from django.contrib import admin
from django.shortcuts import redirect
@admin.register(Product) @admin.register(Product)
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
@ -37,11 +40,63 @@ class OrderItemInline(admin.TabularInline):
extra = 0 extra = 0
readonly_fields = ('product', 'quantity', 'color', 'size', 'price') readonly_fields = ('product', 'quantity', 'color', 'size', 'price')
@admin.register(ShippingAddress)
class ShippingAddressAdmin(admin.ModelAdmin):
list_display = ('street_address', 'city', 'postal_code', 'country')
search_fields = ('street_address', 'city', 'postal_code', 'country')
@admin.register(Order) @admin.register(Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'date_ordered', 'status', 'total_price') list_display = ('id', 'date_ordered', 'status', 'total_price', 'get_shipping_address')
inlines = [OrderItemInline] inlines = [OrderItemInline]
list_filter = ('status', 'payment_status') list_filter = ('status', 'payment_status')
readonly_fields = ('shipping_address_details',)
def get_shipping_address(self, obj):
if obj.shipping_address:
return f"{obj.shipping_address.street_address}, {obj.shipping_address.city}"
return "No shipping address"
get_shipping_address.short_description = 'Shipping Address'
def shipping_address_details(self, obj):
if obj.shipping_address:
return format_html(
"""
<div style="padding: 10px; background-color: #f9f9f9; border-radius: 4px;">
<strong>Street:</strong> {}<br>
{}
<strong>City:</strong> {}<br>
<strong>State:</strong> {}<br>
<strong>Postal Code:</strong> {}<br>
<strong>Country:</strong> {}
</div>
""",
obj.shipping_address.street_address,
f"<strong>Apartment:</strong> {obj.shipping_address.apartment}<br>" if obj.shipping_address.apartment else "",
obj.shipping_address.city,
obj.shipping_address.state,
obj.shipping_address.postal_code,
obj.shipping_address.country,
)
return "No shipping address set"
shipping_address_details.short_description = 'Shipping Address Details'
fieldsets = (
(None, {
'fields': ('user', 'guest_user', 'status', 'payment_status', 'total_price')
}),
('Shipping Information', {
'fields': ('shipping_address_details',),
}),
('Payment Details', {
'fields': ('stripe_payment_intent_id', 'stripe_checkout_session_id', 'stripe_mode'),
'classes': ('collapse',)
}),
('Discount Information', {
'fields': ('coupon', 'discount_amount'),
'classes': ('collapse',)
}),
)
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
# If 'show_preparation' parameter is in the request, show the preparation view # If 'show_preparation' parameter is in the request, show the preparation view
@ -103,6 +158,41 @@ class OrderAdmin(admin.ModelAdmin):
context context
) )
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('prepare-all-orders/', self.admin_site.admin_view(self.prepare_all_orders), name='prepare_all_orders'),
path('prepare-order/<int:order_id>/', self.admin_site.admin_view(self.prepare_order), name='prepare_order'),
path('cancel-and-refund-order/<int:order_id>/', self.admin_site.admin_view(self.cancel_and_refund_order), name='cancel_and_refund_order'),
]
return custom_urls + urls
def prepare_all_orders(self, request):
if request.method == 'POST':
Order.objects.filter(status=OrderStatus.PAID).update(status=OrderStatus.PREPARED)
self.message_user(request, "All orders have been marked as prepared.")
return redirect('admin:shop_order_changelist')
def prepare_order(self, request, order_id):
if request.method == 'POST':
order = Order.objects.get(id=order_id)
order.status = OrderStatus.PREPARED
order.save()
self.message_user(request, f"Order #{order_id} has been marked as prepared.")
return redirect('admin:shop_order_changelist')
def cancel_and_refund_order(self, request, order_id):
if request.method == 'POST':
order = Order.objects.get(id=order_id)
try:
# Reuse the cancel_order logic from your views
from .views import cancel_order
cancel_order(request, order_id)
self.message_user(request, f"Order #{order_id} has been cancelled and refunded.")
except Exception as e:
self.message_user(request, f"Error cancelling order: {str(e)}", level='ERROR')
return redirect('admin:shop_order_changelist')
class GuestUserOrderInline(admin.TabularInline): class GuestUserOrderInline(admin.TabularInline):
model = Order model = Order
extra = 0 extra = 0

@ -1,5 +1,6 @@
from django import forms from django import forms
from .models import Coupon from .models import Coupon
from .models import ShippingAddress
class GuestCheckoutForm(forms.Form): class GuestCheckoutForm(forms.Form):
email = forms.EmailField(required=True) email = forms.EmailField(required=True)
@ -7,3 +8,15 @@ class GuestCheckoutForm(forms.Form):
class CouponApplyForm(forms.Form): class CouponApplyForm(forms.Form):
code = forms.CharField(max_length=50) code = forms.CharField(max_length=50)
class ShippingAddressForm(forms.ModelForm):
class Meta:
model = ShippingAddress
fields = ['street_address', 'apartment', 'city', 'postal_code', 'country']
widgets = {
'street_address': forms.TextInput(attrs={'placeholder': 'Adresse'}),
'apartment': forms.TextInput(attrs={'placeholder': 'Appartement (optionnel)'}),
'city': forms.TextInput(attrs={'placeholder': 'Ville'}),
'postal_code': forms.TextInput(attrs={'placeholder': 'Code postal'}),
'country': forms.TextInput(attrs={'placeholder': 'Pays'}),
}

@ -0,0 +1,41 @@
# Generated by Django 5.1 on 2025-05-06 10:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0026_alter_order_user'),
]
operations = [
migrations.CreateModel(
name='ShippingAddress',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('street_address', models.CharField(max_length=255)),
('apartment', models.CharField(blank=True, max_length=50, null=True)),
('city', models.CharField(max_length=100)),
('state', models.CharField(blank=True, max_length=100, null=True)),
('postal_code', models.CharField(max_length=20)),
('country', models.CharField(max_length=100)),
],
),
migrations.AlterField(
model_name='order',
name='payment_status',
field=models.CharField(choices=[('UNPAID', 'Unpaid'), ('PAID', 'Paid'), ('FAILED', 'Failed'), ('REFUNDED', 'Refunded')], default='UNPAID', max_length=20),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.CharField(choices=[('PENDING', 'Pending'), ('PAID', 'Paid'), ('SHIPPED', 'Shipped'), ('DELIVERED', 'Delivered'), ('CANCELED', 'Canceled'), ('REFUNDED', 'Refunded'), ('PREPARED', 'Prepared')], default='PENDING', max_length=20),
),
migrations.AddField(
model_name='order',
name='shipping_address',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.shippingaddress'),
),
]

@ -8,6 +8,8 @@ class OrderStatus(models.TextChoices):
SHIPPED = 'SHIPPED', 'Shipped' SHIPPED = 'SHIPPED', 'Shipped'
DELIVERED = 'DELIVERED', 'Delivered' DELIVERED = 'DELIVERED', 'Delivered'
CANCELED = 'CANCELED', 'Canceled' CANCELED = 'CANCELED', 'Canceled'
REFUNDED = 'REFUNDED', 'Refunded'
PREPARED = 'PREPARED', 'Prepared'
class CutChoices(models.IntegerChoices): class CutChoices(models.IntegerChoices):
UNISEX = 0, 'Unisex' UNISEX = 0, 'Unisex'
@ -71,6 +73,14 @@ 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 ShippingAddress(models.Model):
street_address = models.CharField(max_length=255)
apartment = models.CharField(max_length=50, blank=True, null=True)
city = models.CharField(max_length=100)
state = models.CharField(max_length=100, blank=True, null=True)
postal_code = models.CharField(max_length=20)
country = models.CharField(max_length=100)
class GuestUser(models.Model): class GuestUser(models.Model):
email = models.EmailField() email = models.EmailField()
phone = models.CharField(max_length=20) phone = models.CharField(max_length=20)
@ -112,6 +122,7 @@ class Coupon(models.Model):
class Order(models.Model): class Order(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
shipping_address = models.ForeignKey(ShippingAddress, on_delete=models.SET_NULL, null=True, blank=True)
date_ordered = models.DateTimeField(auto_now_add=True) date_ordered = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=20, choices=OrderStatus.choices, default=OrderStatus.PENDING) 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) total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
@ -122,6 +133,7 @@ class Order(models.Model):
('UNPAID', 'Unpaid'), ('UNPAID', 'Unpaid'),
('PAID', 'Paid'), ('PAID', 'Paid'),
('FAILED', 'Failed'), ('FAILED', 'Failed'),
('REFUNDED', 'Refunded')
]) ])
webhook_processed = models.BooleanField(default=False) webhook_processed = models.BooleanField(default=False)
stripe_mode = models.CharField(max_length=10, default='test', choices=[ stripe_mode = models.CharField(max_length=10, default='test', choices=[
@ -137,6 +149,13 @@ class Order(models.Model):
def get_total_after_discount(self): def get_total_after_discount(self):
return max(self.total_price - self.discount_amount, 0) return max(self.total_price - self.discount_amount, 0)
def is_cancellable(self):
"""Check if the order can be cancelled"""
return self.status in [OrderStatus.PENDING, OrderStatus.PAID]
def shipping_address_can_be_edited(self):
return self.status in [OrderStatus.PENDING, OrderStatus.PAID, OrderStatus.PREPARED]
class OrderItem(models.Model): class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE)

@ -546,3 +546,229 @@ v .cart-table {
border: 3px solid #90ee90 !important; /* Use your light-green color */ border: 3px solid #90ee90 !important; /* Use your light-green color */
transform: scale(1.1); /* Makes the selected color slightly larger */ transform: scale(1.1); /* Makes the selected color slightly larger */
} }
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
color: white;
display: inline-block;
}
.status-badge.pending {
background-color: #f39200;
}
.status-badge.paid {
background-color: #27ae60;
}
.status-badge.shipped {
background-color: #3498db;
}
.status-badge.delivered {
background-color: #2c3e50;
}
.status-badge.canceled {
background-color: #e74c3c;
}
.status-badge.refunded {
background-color: #e74c3c;
}
.status-badge.prepared {
background-color: #27ae60;
}
.original-price {
text-decoration: line-through;
color: #777;
font-size: 0.9em;
display: block;
font-weight: bold;
}
.discounted-price {
font-weight: bold;
}
.view-btn {
background-color: #3498db;
color: white;
border: none;
padding: 5px 10px;
border-radius: 12px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background-color 0.3s ease;
}
.view-btn:hover {
background-color: #2980b9;
color: white;
}
.inline-form {
display: inline;
}
.empty-orders {
text-align: center;
padding: 20px;
}
.actions {
text-align: center;
}
.order-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
.discount-section {
margin-top: 20px;
border-top: 1px dashed #eee;
padding-top: 15px;
}
.discount-row {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.total-row {
font-weight: bold;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #eee;
}
.coupon-info {
margin-top: 10px;
font-size: 0.9em;
color: #666;
}
.order-actions {
margin-top: 20px;
text-align: right;
padding-top: 15px;
border-top: 1px solid #eee;
}
.order-items-section {
margin-bottom: 20px;
}
.order-items-section h3 {
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.order-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
border-radius: 12px;
overflow: hidden;
padding: 0px;
}
.order-table tbody tr.odd-row {
background-color: #f0f0f0;
}
.order-table tbody tr.even-row {
background-color: #e8e8e8;
}
.shipping-address-section {
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.address-details {
margin: 10px 0;
}
.address-actions {
margin-top: 10px;
}
.edit-address-btn,
.add-address-btn {
background-color: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.edit-address-btn:hover,
.add-address-btn:hover {
background-color: #0056b3;
}
.shipping-address-form {
margin-top: 15px;
padding: 15px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
.save-btn {
background-color: #27ae60;
color: white;
border: none;
padding: 5px 15px;
border-radius: 4px;
cursor: pointer;
}
.cancel-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 5px 15px;
border-radius: 4px;
cursor: pointer;
}
.save-btn:hover {
background-color: #218838;
}
.cancel-btn:hover {
background-color: #c82333;
}
.address-input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 12px;
font-size: 14px;
}
.address-section {
margin: 20px 0;
padding: 15px;
border-radius: 5px;
}
#address-message {
margin-top: 10px;
font-size: 14px;
}

@ -76,5 +76,65 @@ class StripeService:
"""Retrieve a payment intent by ID""" """Retrieve a payment intent by ID"""
return stripe.PaymentIntent.retrieve(payment_intent_id) return stripe.PaymentIntent.retrieve(payment_intent_id)
def create_refund(self, payment_intent_id, amount=None, reason=None):
"""
Create a refund for a payment intent
Args:
payment_intent_id (str): The payment intent ID to refund
amount (int, optional): Amount to refund in cents. If None, refunds the entire amount.
reason (str, optional): The reason for the refund, one of 'duplicate', 'fraudulent', or 'requested_by_customer'
Returns:
stripe.Refund: The created refund object
"""
try:
refund_params = {
'payment_intent': payment_intent_id,
}
if amount is not None:
refund_params['amount'] = amount
if reason in ['duplicate', 'fraudulent', 'requested_by_customer']:
refund_params['reason'] = reason
# Log the refund attempt
mode_str = "TEST" if self.is_test_mode else "LIVE"
logger.info(f"[{mode_str}] Creating refund for payment intent {payment_intent_id}")
# Process the refund
refund = stripe.Refund.create(**refund_params)
# Log success
logger.info(f"Refund created successfully: {refund.id}")
return refund
except stripe.error.StripeError as e:
# Log the error
logger.error(f"Stripe error creating refund: {str(e)}")
raise
except Exception as e:
# Log any other errors
logger.error(f"Unexpected error creating refund: {str(e)}")
raise
def get_refund(self, refund_id):
"""
Retrieve a refund by ID
Args:
refund_id (str): The ID of the refund to retrieve
Returns:
stripe.Refund: The refund object
"""
try:
return stripe.Refund.retrieve(refund_id)
except stripe.error.StripeError as e:
logger.error(f"Stripe error retrieving refund {refund_id}: {str(e)}")
raise
# Create a singleton instance for import and use throughout the app # Create a singleton instance for import and use throughout the app
stripe_service = StripeService() stripe_service = StripeService()

@ -5,8 +5,20 @@
<p>Total orders with status PAID: {{ total_orders }}</p> <p>Total orders with status PAID: {{ total_orders }}</p>
<p>Total items to prepare: {{ total_items }}</p> <p>Total items to prepare: {{ total_items }}</p>
<button onclick="window.print()" style="margin-bottom: 20px">Print This Page</button> <div style="margin-bottom: 20px;">
<a href="?" class="button" style="margin-left: 10px">Back to Orders</a> <button onclick="window.print()" style="margin-right: 10px">Print This Page</button>
<a href="?" class="button" style="margin-right: 10px">Back to Orders</a>
<!-- Global prepare button -->
<form method="POST" action="{% url 'admin:prepare_all_orders' %}" style="display: inline;">
{% csrf_token %}
<button type="submit"
onclick="return confirm('Are you sure you want to mark all orders as prepared?')"
style="background-color: #28a745; color: white; border: none; padding: 5px 10px; cursor: pointer;">
Mark All Orders as Prepared
</button>
</form>
</div>
<h2>Items Summary</h2> <h2>Items Summary</h2>
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse;">
@ -35,7 +47,7 @@
<td style="padding: 8px; border-bottom: 1px solid #ddd; font-weight: bold;">{{ item.quantity }}</td> <td style="padding: 8px; border-bottom: 1px solid #ddd; font-weight: bold;">{{ item.quantity }}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"> <td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% for order_id in item.orders %} {% for order_id in item.orders %}
<a href="../{{ order_id }}/change/">Order #{{ order_id }}</a>{% if not forloop.last %}, {% endif %} <a href="../order/{{ order_id }}/change/">Order #{{ order_id }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
@ -54,13 +66,15 @@
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Order #</th> <th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Order #</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Date</th> <th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Date</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Customer</th> <th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Customer</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Shipping Address</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Items</th> <th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Items</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for order in orders %} {% for order in orders %}
<tr> <tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><a href="../{{ order.id }}/change/">Order #{{ order.id }}</a></td> <td style="padding: 8px; border-bottom: 1px solid #ddd;"><a href="../order/{{ order.id }}/change/">Order #{{ order.id }}</a></td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{{ order.date_ordered|date:"Y-m-d H:i" }}</td> <td style="padding: 8px; border-bottom: 1px solid #ddd;">{{ order.date_ordered|date:"Y-m-d H:i" }}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"> <td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% if order.user %} {% if order.user %}
@ -71,6 +85,16 @@
Unknown Unknown
{% endif %} {% endif %}
</td> </td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% if order.shipping_address %}
{{ order.shipping_address.street_address }}
{% if order.shipping_address.apartment %}, {{ order.shipping_address.apartment }}{% endif %}<br>
{{ order.shipping_address.postal_code }} {{ order.shipping_address.city }}<br>
{{ order.shipping_address.state }}, {{ order.shipping_address.country }}
{% else %}
No shipping address
{% endif %}
</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"> <td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% for item in order.items.all %} {% for item in order.items.all %}
{{ item.quantity }}x {{ item.product.title }} {{ item.quantity }}x {{ item.product.title }}
@ -79,10 +103,28 @@
<br> <br>
{% endfor %} {% endfor %}
</td> </td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
<form method="POST" action="{% url 'admin:prepare_order' order.id %}" style="display: inline;">
{% csrf_token %}
<button type="submit"
style="background-color: #28a745; color: white; border: none; padding: 5px 10px; margin-right: 5px; cursor: pointer;">
Mark as Prepared
</button>
</form>
<form method="POST" action="{% url 'admin:cancel_and_refund_order' order.id %}" style="display: inline;">
{% csrf_token %}
<button type="submit"
onclick="return confirm('Are you sure you want to cancel and refund this order?')"
style="background-color: #dc3545; color: white; border: none; padding: 5px 10px; cursor: pointer;">
Cancel & Refund
</button>
</form>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4" style="padding: 8px; text-align: center;">No orders found</td> <td colspan="6" style="padding: 8px; text-align: center;">No orders found</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

@ -5,17 +5,7 @@
{% block second_title %}La Boutique{% endblock %} {% block second_title %}La Boutique{% endblock %}
{% block content %} {% block content %}
<nav class="margin10"> {% include 'shop/partials/navigation_base.html' %}
<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>
{% if STRIPE_IS_TEST_MODE %} {% if STRIPE_IS_TEST_MODE %}
<div class="alert alert-warning" style="background-color: #fff3cd; color: #856404; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ffeeba;"> <div class="alert alert-warning" style="background-color: #fff3cd; color: #856404; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ffeeba;">
<strong> Test Mode:</strong> Stripe is currently in test mode. No real payments will be processed. <strong> Test Mode:</strong> Stripe is currently in test mode. No real payments will be processed.
@ -26,18 +16,18 @@
<div class="grid-x"> <div class="grid-x">
<div class="small-12 medium-9 large-6 padding10"> <div class="small-12 medium-9 large-6 padding10">
<h1 class="club padding10 topmargin20">Votre panier</h1> <h1 class="club padding10">Votre panier</h1>
<div class="bubble"> <div class="bubble">
{% if display_data.items %} {% if display_data.items %}
<div class="info-box" style="background-color: #f8f9fa; border-left: 4px solid #4e73df; padding: 15px; margin: 15px 0; border-radius: 5px;"> <div class="info-box" style="background-color: #f8f9fa; border-left: 4px solid #4e73df; padding: 15px; margin: 15px 0; border-radius: 4px;">
<h4 style="color: #4e73df; margin-top: 0;">Comment fonctionne la livraison ?</h4> <h4 style="color: #4e73df; margin-top: 0;">Comment fonctionne la livraison ?</h4>
<p>Cette boutique fonctionne entre amis 'Padel Club'. Les commandes sont :</p> <p>Cette boutique fonctionne entre amis 'Padel Club'. Les commandes sont :</p>
<ol style="padding-left: 20px; margin-bottom: 0;"> <ol style="padding-left: 20px; margin-bottom: 0;">
<li>Passées en ligne via notre système</li> <li>Passées en ligne via notre système</li>
<li>Préparées par notre équipe</li> <li>Préparées par notre équipe</li>
<li>Remises en main propre lors d'une prochaine session de padel</li> <li>Remises en main propre lors d'une prochaine session de padel ou livrées à l'adresse indiquée dans la mesure du possible</li>
</ol> </ol>
<p style="margin-top: 10px; margin-bottom: 0;">Pas d'expédition : nous vous remettrons votre commande personnellement au club !</p> <p style="margin-top: 10px; margin-bottom: 0;">Livraison : En général, nous vous remettrons votre commande personnellement au club. Les livraisons peuvent être possible en fonction du lieu, n'hésitez donc pas à indiquer une adresse de livraison.</p>
</div> </div>
{% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=True %} {% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=True %}
@ -67,6 +57,24 @@
</div> </div>
</div> </div>
<div class="coupon-section">
<div>Adresse de livraison (dans la mesure du possible)</div>
<div class="coupon-form"">
<input type="text" id="street-address" class="address-input" style="flex-grow: 1; width: 100%;" placeholder="Adresse">
<div style="margin-bottom: 10px;">
<input type="text" id="apartment" class="address-input" style="width: 100%;" placeholder="Appartement (optionnel)">
</div>
<div style="display: flex; gap: 10px;">
<input type="text" id="postal-code" class="address-input" style="width: 25%;" placeholder="Code postal">
<input type="text" id="city" class="address-input" style="width: 25%;" placeholder="Ville">
<input type="text" id="country" class="address-input" style="width: 25%;" placeholder="Pays">
</div>
</div>
<div id="address-message"></div>
</div>
<div class="cart-summary"> <div class="cart-summary">
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -123,6 +131,17 @@
const subtotalAmount = document.getElementById('subtotal-amount'); const subtotalAmount = document.getElementById('subtotal-amount');
const discountAmount = document.getElementById('discount-amount'); const discountAmount = document.getElementById('discount-amount');
const finalTotal = document.getElementById('final-total'); const finalTotal = document.getElementById('final-total');
const shippingForm = document.getElementById('shipping-form');
// Function to collect shipping address data
function getShippingData() {
const formData = new FormData(shippingForm);
const shippingData = {};
formData.forEach((value, key) => {
shippingData[key] = value;
});
return shippingData;
}
// Initial values // Initial values
const originalTotal = parseFloat('{{ display_data.total_price }}'); const originalTotal = parseFloat('{{ display_data.total_price }}');
@ -244,6 +263,9 @@
checkoutButton.textContent = 'Chargement...'; checkoutButton.textContent = 'Chargement...';
checkoutButton.disabled = true; checkoutButton.disabled = true;
// Get shipping data
const shippingData = getShippingData();
// Create order and get checkout session // Create order and get checkout session
fetch('{% url "shop:create_checkout_session" %}', { fetch('{% url "shop:create_checkout_session" %}', {
method: 'POST', method: 'POST',
@ -251,6 +273,9 @@
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}' 'X-CSRFToken': '{{ csrf_token }}'
}, },
body: JSON.stringify({
shipping_address: shippingData
}),
credentials: 'same-origin', credentials: 'same-origin',
}) })
.then(function(response) { .then(function(response) {

@ -6,18 +6,9 @@
{% block content %} {% block content %}
<nav class="margin10"> {% include 'shop/partials/navigation_base.html' %}
<a href="{% url 'shop:product_list' %}">La Boutique</a>
<a href="{% url 'index' %}" class="orange">Accueil</a> <h1 class="club padding10">Validation de la commande</h1>
<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 padding10 topmargin20">Validation de la commande</h1>
<div class="grid-x"> <div class="grid-x">
<div class="small-12 medium-6 large-6 padding10"> <div class="small-12 medium-6 large-6 padding10">
<div class="bubble checkout-container"> <div class="bubble checkout-container">

@ -0,0 +1,81 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Mes Commandes{% endblock %}
{% block first_title %}Padel Club{% endblock %}
{% block second_title %}Mes Commandes{% endblock %}
{% block content %}
{% include 'shop/partials/navigation_base.html' %}
<div class="grid-x">
<div class="cell medium-12 large-6 padding10">
<h1 class="club padding10">Mes Commandes</h1>
<div class="bubble">
{% if orders %}
<table class="order-table">
<tbody>
{% for order in orders %}
<tr class="{% cycle 'odd-row' 'even-row' %}">
<td class="text-left">Commande #{{ order.id }}</td>
<td>
<a href="{% url 'shop:order_detail' order.id %}" class="view-btn">Détails</a>
</td>
<td class="text-left">{{ order.date_ordered|date:"d/m/Y H:i" }}</td>
<td class="text-left">
{% if order.status == 'PENDING' %}
<span class="status-badge pending">En attente</span>
{% elif order.status == 'PAID' %}
<span class="status-badge paid">Payée</span>
{% elif order.status == 'PREPARED' %}
<span class="status-badge prepared">En cours de préparation</span>
{% elif order.status == 'SHIPPED' %}
<span class="status-badge shipped">Expédiée</span>
{% elif order.status == 'DELIVERED' %}
<span class="status-badge delivered">Livrée</span>
{% elif order.status == 'CANCELED' %}
<span class="status-badge canceled">Annulée</span>
{% elif order.status == 'REFUNDED' %}
<span class="status-badge refunded">Remboursée</span>
{% endif %}
</td>
<td class="price-column">
{% if order.discount_amount > 0 %}
<span class="original-price">{{ order.total_price }}€</span>
<span class="discounted-price">{{ order.get_total_after_discount }}€</span>
{% else %}
{{ order.total_price }}€
{% endif %}
</td>
<td class="actions">
{% if order.status == 'PENDING' or order.status == 'PAID' %}
<form method="post" action="{% url 'shop:cancel_order' order.id %}" class="inline-form" onsubmit="return confirm('Êtes-vous sûr de vouloir annuler cette commande?');">
{% csrf_token %}
<button type="submit" class="remove-btn">Annuler</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-orders">
<p>Vous n'avez pas encore de commandes.</p>
<a href="{% url 'shop:product_list' %}" class="confirm-nav-button">Découvrir la boutique</a>
</div>
{% endif %}
</div>
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

@ -0,0 +1,150 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Détail de commande{% endblock %}
{% block first_title %}Padel Club{% endblock %}
{% block second_title %}Détail de commande{% endblock %}
{% block content %}
{% include 'shop/partials/navigation_base.html' %}
<div class="grid-x">
<div class="cell medium-8 large-8 padding10">
<h1 class="club padding10">Commande #{{ order.id }}</h1>
<div class="bubble">
<div class="order-meta">
<div class="order-date">
<strong>Date:</strong> {{ order.date_ordered|date:"d/m/Y H:i" }}
</div>
<div class="order-status">
<strong>Statut:</strong>
{% if order.status == 'PENDING' %}
<span class="status-badge pending">En attente</span>
{% elif order.status == 'PREPARED' %}
<span class="status-badge prepared">En cours de préparation</span>
{% elif order.status == 'PAID' %}
<span class="status-badge paid">Payée</span>
{% elif order.status == 'SHIPPED' %}
<span class="status-badge shipped">Expédiée</span>
{% elif order.status == 'DELIVERED' %}
<span class="status-badge delivered">Livrée</span>
{% elif order.status == 'CANCELED' %}
<span class="status-badge canceled">Annulée</span>
{% elif order.status == 'REFUNDED' %}
<span class="status-badge refunded">Remboursée</span>
{% endif %}
</div>
</div>
<div class="order-items-section">
<h3>Produits</h3>
{% with items=order_items total_quantity=total_quantity total_price=order.total_price %}
{% include 'shop/partials/order_items_display.html' with items=items total_quantity=total_quantity total_price=total_price edit_mode=False cancel_mode=order.is_cancellable %}
{% endwith %}
</div>
<div class="coupon-section">
<div>Adresse de livraison (dans la mesure du possible)</div>
{% if order.shipping_address %}
<div class="address-details" style="margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 4px;">
<p style="margin: 0;">{{ order.shipping_address.street_address }}</p>
{% if order.shipping_address.apartment %}
<p style="margin: 5px 0;">{{ order.shipping_address.apartment }}</p>
{% endif %}
<p style="margin: 0;">{{ order.shipping_address.postal_code }} {{ order.shipping_address.city }}, {{ order.shipping_address.country }}</p>
</div>
{% if order.shipping_address_can_be_edited %}
<button class="edit-address-btn confirm-nav-button" style="margin-top: 10px;" onclick="toggleAddressForm()">Modifier l'adresse</button>
<div id="address-form-container" style="display: none; margin-top: 10px;">
<form method="post" action="{% url 'shop:update_shipping_address' order.id %}" class="coupon-form">
{% csrf_token %}
<input type="text" name="street_address" class="address-input" style="width: 100%;" placeholder="Adresse" value="{{ order.shipping_address.street_address }}">
<div style="margin: 10px 0;">
<input type="text" name="apartment" class="address-input" style="width: 100%;" placeholder="Appartement (optionnel)" value="{{ order.shipping_address.apartment|default:'' }}">
</div>
<div style="display: flex; gap: 10px;">
<input type="text" name="postal_code" class="address-input" style="width: 25%;" placeholder="Code postal" value="{{ order.shipping_address.postal_code }}">
<input type="text" name="city" class="address-input" style="width: 25%;" placeholder="Ville" value="{{ order.shipping_address.city }}">
<input type="text" name="country" class="address-input" style="width: 25%;" placeholder="Pays" value="{{ order.shipping_address.country }}">
</div>
<button type="submit" class="save-btn confirm-nav-button">Enregistrer</button>
</form>
</div>
{% endif %}
{% else %}
<p>Aucune adresse de livraison renseignée</p>
{% if order.shipping_address_can_be_edited %}
<button class="add-address-btn confirm-nav-button" onclick="toggleAddressForm()">Ajouter une adresse</button>
<div id="address-form-container" style="display: none; margin-top: 10px;">
<form method="post" action="{% url 'shop:update_shipping_address' order.id %}" class="coupon-form">
{% csrf_token %}
<input type="text" name="street_address" class="address-input" style="width: 100%;" placeholder="Adresse">
<div style="margin: 10px 0;">
<input type="text" name="apartment" class="address-input" style="width: 100%;" placeholder="Appartement (optionnel)">
</div>
<div style="display: flex; gap: 10px;">
<input type="text" name="postal_code" class="address-input" style="width: 25%;" placeholder="Code postal">
<input type="text" name="city" class="address-input" style="width: 25%;" placeholder="Ville">
<input type="text" name="country" class="address-input" style="width: 25%;" placeholder="Pays">
</div>
<button type="submit" class="save-btn confirm-nav-button">Enregistrer</button>
</form>
</div>
{% endif %}
{% endif %}
</div>
{% if order.discount_amount > 0 %}
<div class="discount-section">
<div class="discount-row">
<span>Sous-total:</span>
<span class="price-value">{{ order.total_price }}€</span>
</div>
<div class="discount-row">
<span>Réduction:</span>
<span class="price-value">-{{ order.discount_amount }}€</span>
</div>
<div class="discount-row total-row">
<span>Total final:</span>
<span class="price-value">{{ order.get_total_after_discount }}€</span>
</div>
{% if order.coupon %}
<div class="coupon-info">
Coupon appliqué: {{ order.coupon.code }}
</div>
{% endif %}
</div>
{% endif %}
<div class="order-actions">
{% if order.status == 'PENDING' or order.status == 'PAID' %}
<form method="post" action="{% url 'shop:cancel_order' order.id %}" class="inline-form" onsubmit="return confirm('Êtes-vous sûr de vouloir annuler cette commande? Cette action est irréversible.');">
{% csrf_token %}
<button type="submit" class="remove-btn">Annuler la commande</button>
</form>
{% endif %}
</div>
</div>
</div>
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
<script>
function toggleAddressForm() {
const formContainer = document.getElementById('address-form-container');
const isHidden = formContainer.style.display === 'none';
formContainer.style.display = isHidden ? 'block' : 'none';
}
</script>
{% endblock %}

@ -0,0 +1,9 @@
<nav class="margin10">
<a href="{% url 'shop:product_list' %}">La Boutique</a>
<a href="{% url 'shop:my_orders' %}">Mes Commandes</a>
{% if user.is_authenticated %}
<a href="{% url 'profile' %}">Mon compte</a>
{% else %}
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
</nav>

@ -1,60 +1,68 @@
<table class="cart-table"> <table class="cart-table">
<tbody> <tbody>
{% for item in items %} {% for item in items %}
<tr class="{% cycle 'odd-row' 'even-row' %}"> <tr class="{% cycle 'odd-row' 'even-row' %}">
<td class="text-left product-name" data-label="Produit">{{ item.product_title }}</td> <td class="text-left product-name" data-label="Produit">{{ item.product_title }}</td>
{% if item.product_description %} {% if item.product_description %}
<td class="text-left product-description" data-label="Description">{{ item.product_description }}</td> <td class="text-left product-description" data-label="Description">{{ item.product_description }}</td>
{% endif %} {% endif %}
<td class="product-color" data-label="Couleur"> <td class="product-color" data-label="Couleur">
<div class="color-display"> <div class="color-display">
<div class="color-sample-cart" <div class="color-sample-cart"
{% if item.secondary_color_hex %} {% if item.secondary_color_hex %}
style="background-image: linear-gradient(to right, {{ item.color_hex }} 50%, {{ item.secondary_color_hex }} 50%);" style="background-image: linear-gradient(to right, {{ item.color_hex }} 50%, {{ item.secondary_color_hex }} 50%);"
{% else %} {% else %}
style="background-color: {{ item.color_hex }};" style="background-color: {{ item.color_hex }};"
{% endif %} {% endif %}
></div> ></div>
{{ item.color_name }} | {{ item.size_name }} {{ item.color_name }} | {{ item.size_name }}
</div> </div>
</td> </td>
<td class="product-quantity" data-label="Quantité"> <td class="product-quantity" data-label="Quantité">
{% if edit_mode %} {% if edit_mode %}
<div class="quantity-controls"> <div class="quantity-controls">
<form method="post" action="{% url 'shop:update_cart_item' %}" class="quantity-form"> <form method="post" action="{% url 'shop:update_cart_item' %}" class="quantity-form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="item_id" value="{{ item.id }}"> <input type="hidden" name="item_id" value="{{ item.id }}">
<button type="submit" name="action" value="decrease" class="quantity-btn" {% if item.quantity <= 1 %}disabled{% endif %}>-</button> <button type="submit" name="action" value="decrease" class="quantity-btn" {% if item.quantity <= 1 %}disabled{% endif %}>-</button>
<span class="quantity-value">{{ item.quantity }}</span> <span class="quantity-value">{{ item.quantity }}</span>
<button type="submit" name="action" value="increase" class="quantity-btn">+</button> <button type="submit" name="action" value="increase" class="quantity-btn">+</button>
</form> </form>
</div> </div>
{% else %} {% else %}
<span>x {{ item.quantity }}</span> <span>x {{ item.quantity }}</span>
{% endif %} {% endif %}
</td> </td>
<td class="price-column product-price" data-label="Prix">{{ item.total_price }} €</td> <td class="price-column product-price" data-label="Prix">{{ item.total_price }} €</td>
{% if edit_mode %} {% if edit_mode %}
<td class="product-actions"> <td class="product-actions">
<form method="post" action="{% url 'shop:remove_from_cart' %}" class="remove-form"> <form method="post" action="{% url 'shop:remove_from_cart' %}" class="remove-form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="item_id" value="{{ item.id }}"> <input type="hidden" name="item_id" value="{{ item.id }}">
<button type="submit" class="remove-btn">retirer</button> <button type="submit" class="remove-btn">retirer</button>
</form> </form>
</td> </td>
{% endif %} {% elif cancel_mode and items.count > 1 %}
</tr> <td class="product-actions">
{% endfor %} <form method="post" action="{% url 'shop:cancel_order_item' order.id item.id %}" class="remove-form"
</tbody> onsubmit="return confirm('Êtes-vous sûr de vouloir annuler cet article ? {% if order.status == 'PAID' %}Un remboursement sera effectué.{% endif %}');">
<tfoot> {% csrf_token %}
<tr> <button type="submit" class="remove-btn">annuler</button>
<td class="total-quantity" data-label="total-quantity">{{ total_quantity }} produit(s)</td> </form>
<td class="total-label text-left"></td> </td>
<td class="total-label text-left"></td> {% endif %}
<td class="price-column total-price" data-label="total-price">{{ total_price }} €</td> </tr>
{% if edit_mode %} {% endfor %}
<td class="total-label text-left"></td> </tbody>
{% endif %} <tfoot>
</tr> <tr>
</tfoot> <td class="total-quantity" data-label="total-quantity">{{ total_quantity }} produit(s)</td>
<td class="total-label text-left"></td>
<td class="total-label text-left"></td>
<td class="price-column total-price" data-label="total-price">{{ total_price }} €</td>
{% if edit_mode or cancel_mode %}
<td class="total-label text-left"></td>
{% endif %}
</tr>
</tfoot>
</table> </table>

@ -12,21 +12,12 @@
<small>Use test card: 4242 4242 4242 4242 with any future date and any CVC.</small> <small>Use test card: 4242 4242 4242 4242 with any future date and any CVC.</small>
</div> </div>
{% endif %} {% endif %}
<nav class="margin10"> {% include 'shop/partials/navigation_base.html' %}
<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="grid-x">
<div class="cell medium-6 large-6 padding10"> <div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Résumé de votre commande</h1> <h1 class="club padding10">Résumé de votre commande</h1>
<div class="bubble"> <div class="bubble">
{% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=False %} {% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=False %}

@ -7,21 +7,11 @@
{% block content %} {% block content %}
<nav class="margin10"> {% include 'shop/partials/navigation_base.html' %}
<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="grid-x">
<div class="cell medium-6 large-6 padding10"> <div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Paiement</h1> <h1 class="club padding10">Paiement</h1>
<div class="bubble"> <div class="bubble">
<h2>Le paiement a été annulé</h2> <h2>Le paiement a été annulé</h2>
<p>Votre commande n'a pas été finalisée car le paiement a été annulé.</p> <p>Votre commande n'a pas été finalisée car le paiement a été annulé.</p>

@ -5,21 +5,11 @@
{% block second_title %}La Boutique{% endblock %} {% block second_title %}La Boutique{% endblock %}
{% block content %} {% block content %}
<nav class="margin10"> {% include 'shop/partials/navigation_base.html' %}
<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="grid-x">
<div class="cell medium-6 large-6 padding10"> <div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Paiement réussi</h1> <h1 class="club padding10">Paiement réussi</h1>
<div class="bubble"> <div class="bubble">
<h2>Merci pour votre commande !</h2> <h2>Merci pour votre commande !</h2>
<p>Votre paiement a été traité avec succès.</p> <p>Votre paiement a été traité avec succès.</p>

@ -6,22 +6,12 @@
{% block content %} {% block content %}
<nav class="margin10"> {% include 'shop/partials/navigation_base.html' %}
<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="info-box" style="border-left: 4px solid #f39200; padding: 12px; margin: 20px 12px;"> <div class="info-box" style="border-left: 4px solid #f39200; padding: 12px; margin: 20px 12px;">
<h3 style="color: #505050; margin-top: 0;">Bienvenue sur la boutique Padel Club des copains !</h3> <h3 style="color: #505050; margin-top: 0;">Bienvenue sur la boutique Padel Club des copains !</h3>
<p style="margin-top: 10px; margin-bottom: 0;"><strong>Photos :</strong> Les photos des vêtements n'ont pas encore tous le logo, la description indique où il situera. <p style="margin-top: 10px; margin-bottom: 0;"><strong>Photos :</strong> Les photos des vêtements n'ont pas encore tous le logo, la description indique où il situera.
<p style="margin-top: 10px; margin-bottom: 0;"><strong>Livraison :</strong> Commandez en ligne et récupérez votre commande en main propre lors de votre prochaine session de padel au club !</p> <p style="margin-top: 10px; margin-bottom: 0;"><strong>Livraison :</strong> Commandez en ligne et récupérez votre commande en main propre lors de votre prochaine session de padel au club. La livraison peut être possible !</p>
</div> </div>
<nav class="margin10"> <nav class="margin10">

@ -24,5 +24,9 @@ urlpatterns = [
path('debug/simulate-payment-failure/', views.simulate_payment_failure, name='simulate_payment_failure'), path('debug/simulate-payment-failure/', views.simulate_payment_failure, name='simulate_payment_failure'),
path('apply-coupon/', views.apply_coupon, name='apply_coupon'), path('apply-coupon/', views.apply_coupon, name='apply_coupon'),
path('remove-coupon/', views.remove_coupon, name='remove_coupon'), path('remove-coupon/', views.remove_coupon, name='remove_coupon'),
path('my-orders/', views.my_orders, name='my_orders'),
path('order/<int:order_id>/', views.order_detail, name='order_detail'),
path('cancel-order/<int:order_id>/', views.cancel_order, name='cancel_order'),
path('cancel-order-item/<int:order_id>/<int:item_id>/', views.cancel_order_item, name='cancel_order_item'),
path('order/<int:order_id>/update-shipping/', views.update_shipping_address, name='update_shipping_address'),
] ]

@ -13,6 +13,8 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.http import HttpResponse from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone from django.utils import timezone
from .forms import ShippingAddressForm
import json # Add this with your other imports
from . import cart from . import cart
@ -143,16 +145,20 @@ def view_cart(request):
total = cart.get_cart_total(request) total = cart.get_cart_total(request)
total_quantity = cart_items.aggregate(total_quantity=Sum('quantity'))['total_quantity'] total_quantity = cart_items.aggregate(total_quantity=Sum('quantity'))['total_quantity']
display_data = prepare_item_display_data(cart_items, is_cart=True) display_data = prepare_item_display_data(cart_items, is_cart=True)
context = { context = {
'display_data': display_data, 'display_data': display_data,
'total': total, 'total': total,
'total_quantity': total_quantity, 'total_quantity': total_quantity,
'settings': settings, # Add this line to pass settings to template 'settings': settings,
} }
# Add Stripe publishable key for authenticated users # Add shipping form and Stripe key for authenticated users
if request.user.is_authenticated: if request.user.is_authenticated:
context['stripe_publishable_key'] = settings.STRIPE_PUBLISHABLE_KEY context.update({
'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY,
'shipping_form': ShippingAddressForm()
})
return render(request, 'shop/cart.html', context) return render(request, 'shop/cart.html', context)
@ -457,19 +463,34 @@ def create_checkout_session(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return JsonResponse({'error': 'User must be authenticated'}, status=403) return JsonResponse({'error': 'User must be authenticated'}, status=403)
# Create the order # Parse shipping address data from request
order = create_order(request) try:
data = json.loads(request.body)
shipping_data = data.get('shipping_address', {})
except json.JSONDecodeError:
shipping_data = {}
# Validate shipping address
shipping_form = ShippingAddressForm(shipping_data)
if not shipping_form.is_valid():
return JsonResponse({'error': 'Invalid shipping address'}, status=400)
# Save shipping address
shipping_address = shipping_form.save()
# Create the order with shipping address
order = create_order(request)
if not order: if not order:
return JsonResponse({'error': 'Could not create order from cart'}, status=400) return JsonResponse({'error': 'Could not create order from cart'}, status=400)
# Get order items # Attach shipping address to order
order_items = order.items.all() order.shipping_address = shipping_address
order.save()
# Create line items # Create line items and checkout session as before
order_items = order.items.all()
line_items = _create_stripe_line_items(order_items) line_items = _create_stripe_line_items(order_items)
# Create checkout session
try: try:
checkout_session = _create_stripe_checkout_session(request, order, line_items) checkout_session = _create_stripe_checkout_session(request, order, line_items)
return JsonResponse({'id': checkout_session.id}) return JsonResponse({'id': checkout_session.id})
@ -752,3 +773,227 @@ def remove_coupon(request):
if 'coupon_id' in request.session: if 'coupon_id' in request.session:
del request.session['coupon_id'] del request.session['coupon_id']
return JsonResponse({'status': 'success', 'message': 'Coupon supprimé'}) return JsonResponse({'status': 'success', 'message': 'Coupon supprimé'})
def my_orders(request):
"""Display all orders for the logged-in user"""
if not request.user.is_authenticated:
messages.error(request, "Vous devez être connecté pour voir vos commandes.")
return redirect('login')
# Get all orders for the current user, ordered by date (newest first)
orders = Order.objects.filter(user=request.user).order_by('-date_ordered')
return render(request, 'shop/my_orders.html', {
'orders': orders,
})
def order_detail(request, order_id):
"""Display details for a specific order"""
if not request.user.is_authenticated:
messages.error(request, "Vous devez être connecté pour voir vos commandes.")
return redirect('login')
# Get the order, ensuring it belongs to the current user
order = get_object_or_404(Order, id=order_id, user=request.user)
# Get order items
order_items = order.items.all()
# Calculate total quantity
total_quantity = sum(item.quantity for item in order_items)
# Transform order items to match the display format expected by the template
items_for_display = []
for item in order_items:
items_for_display.append({
'id': item.id,
'product_title': item.product.title,
'product_description': item.product.description,
'color_name': item.color.name if item.color else 'N/A',
'color_hex': item.color.colorHex if item.color else '#FFFFFF',
'secondary_color_hex': item.color.secondary_hex_color if item.color and item.color.secondary_hex_color else None,
'size_name': item.size.name if item.size else 'N/A',
'quantity': item.quantity,
'total_price': item.get_total_price()
})
shipping_form = ShippingAddressForm(instance=order.shipping_address)
return render(request, 'shop/order_detail.html', {
'order': order,
'order_items': items_for_display,
'total_quantity': total_quantity,
'shipping_form': shipping_form,
})
@require_POST
def cancel_order(request, order_id):
"""Cancel an order and process refund if applicable"""
if not request.user.is_authenticated:
messages.error(request, "Vous devez être connecté pour annuler une commande.")
return redirect('login')
order = get_object_or_404(Order, id=order_id, user=request.user)
# Check if order can be cancelled
if not order.is_cancellable():
messages.error(request, "Cette commande ne peut pas être annulée.")
return redirect('shop:my_orders')
print("Order cancellation initiated", order.status, order.stripe_payment_intent_id)
# Process refund for paid orders
if order.status == OrderStatus.PAID and order.stripe_payment_intent_id:
try:
# Attempt to refund through Stripe
refund = stripe_service.create_refund(
order.stripe_payment_intent_id,
reason='requested_by_customer'
)
# Update order status
order.status = OrderStatus.REFUNDED
order.payment_status = 'REFUNDED' # Or 'REFUNDED' if you add this status
order.save()
messages.success(request, "Votre commande a été annulée et remboursée avec succès.")
except Exception as e:
print(f"Refund error: {str(e)}")
messages.error(request, "Un problème est survenu lors du remboursement. Veuillez contacter le support.")
else:
# For pending orders, just cancel without refund
order.status = OrderStatus.CANCELED
order.save()
messages.success(request, "Votre commande a été annulée avec succès.")
# Redirect back to the page they came from, or order list
referring_page = request.META.get('HTTP_REFERER')
if referring_page and 'order_detail' in referring_page:
return redirect('shop:order_detail', order_id=order.id)
else:
return redirect('shop:my_orders')
@require_POST
def cancel_order_item(request, order_id, item_id):
"""Cancel a specific item in an order and process partial refund if applicable"""
if not request.user.is_authenticated:
messages.error(request, "Vous devez être connecté pour annuler un article.")
return redirect('login')
# Get the order and ensure it belongs to the user
order = get_object_or_404(Order, id=order_id, user=request.user)
# Check if order can be modified
if not order.is_cancellable():
messages.error(request, "Cette commande ne peut plus être modifiée.")
return redirect('shop:order_detail', order_id=order.id)
# Get the specific item
try:
order_item = order.items.get(id=item_id)
except OrderItem.DoesNotExist:
messages.error(request, "Article non trouvé.")
return redirect('shop:order_detail', order_id=order.id)
# Calculate refund amount for this item
item_total = order_item.get_total_price()
# If this is the last item, cancel the whole order
remaining_items = order.items.exclude(id=item_id)
if not remaining_items.exists():
return cancel_order(request, order_id)
# Process refund for paid orders
if order.status == OrderStatus.PAID and order.stripe_payment_intent_id:
try:
# Calculate proportional discount if any
if order.discount_amount > 0:
discount_ratio = item_total / order.total_price
item_discount = order.discount_amount * discount_ratio
refund_amount = item_total - item_discount
else:
refund_amount = item_total
# Create partial refund through Stripe
refund = stripe_service.create_refund(
order.stripe_payment_intent_id,
amount=int(refund_amount * 100), # Convert to cents
reason='requested_by_customer'
)
# Update order totals
order.total_price -= item_total
if order.discount_amount > 0:
order.discount_amount -= item_discount
order.save()
# Delete the item
order_item.delete()
messages.success(request, "L'article a été annulé et remboursé avec succès.")
except Exception as e:
print(f"Refund error: {str(e)}")
messages.error(request, "Un problème est survenu lors du remboursement. Veuillez contacter le support.")
else:
# For pending orders, just remove the item and update totals
order.total_price -= item_total
order.save()
order_item.delete()
messages.success(request, "L'article a été annulé avec succès.")
return redirect('shop:order_detail', order_id=order.id)
def checkout_view(request):
if request.method == 'POST':
guest_form = GuestCheckoutForm(request.POST)
shipping_form = ShippingAddressForm(request.POST)
if guest_form.is_valid() and shipping_form.is_valid():
guest_user = guest_form.save()
shipping_address = shipping_form.save()
# Create order with shipping address
order = Order.objects.create(
guest_user=guest_user,
shipping_address=shipping_address,
# ... other order fields
)
return redirect('shop:order_detail', order_id=order.id)
else:
guest_form = GuestCheckoutForm()
shipping_form = ShippingAddressForm()
context = {
'form': guest_form,
'shipping_form': shipping_form,
}
return render(request, 'shop/checkout.html', context)
def update_shipping_address(request, order_id):
order = get_object_or_404(Order, id=order_id)
# Check if order can be edited
if not order.shipping_address_can_be_edited():
messages.error(request, "L'adresse ne peut plus être modifiée pour cette commande.")
return redirect('shop:order_detail', order_id=order.id)
if request.method == 'POST':
form = ShippingAddressForm(request.POST, instance=order.shipping_address)
print("Shipping address")
if form.is_valid():
shipping_address = form.save()
print("Shipping address updated")
if not order.shipping_address:
order.shipping_address = shipping_address
order.save()
messages.success(request, "L'adresse de livraison a été mise à jour.")
return redirect('shop:order_detail', order_id=order.id)
else:
form = ShippingAddressForm(instance=order.shipping_address)
context = {
'form': form,
'order': order
}
return redirect('shop:order_detail', order_id=order.id)

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-05-06 07:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0118_tournament_animation_type'),
]
operations = [
migrations.AlterField(
model_name='tournament',
name='animation_type',
field=models.IntegerField(choices=[(0, 'Tournoi'), (1, 'Mêlée'), (2, 'Classement'), (3, 'Consolation')], default=0),
),
]

@ -45,24 +45,26 @@
</div> </div>
{% endif %} {% endif %}
<div class="cell medium-6 large-6 topblock padding10"> <div class="grid-x">
<div class="bubble"> <div class="cell medium-6 large-6 topblock padding10">
<label class="title">Mes informations</label> <div class="bubble">
<form method="post"> <label class="title">Mes informations</label>
{% csrf_token %} <form method="post">
{{ form.as_p }} {% csrf_token %}
<button type="submit" class="rounded-button">Sauver les changements</button> {{ form.as_p }}
</form> <button type="submit" class="rounded-button">Sauver les changements</button>
</form>
</div>
</div> </div>
</div> <div class="cell medium-6 large-6 topblock padding10">
<div class="cell medium-6 large-6 topblock padding10"> <div class="bubble">
<div class="bubble"> <label class="title">Mot de passe</label>
<label class="title">Mot de passe</label> <form method="post" action="{% url 'custom_password_change' %}">
<form method="post" action="{% url 'custom_password_change' %}"> {% csrf_token %}
{% csrf_token %} {{ password_change_form.as_p }}
{{ password_change_form.as_p }} <button type="submit" class="rounded-button">Modifier le mot de passe</button>
<button type="submit" class="rounded-button">Modifier le mot de passe</button> </form>
</form> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

@ -9,4 +9,5 @@
<a href="{% url 'custom-login' %}">Se connecter</a> <a href="{% url 'custom-login' %}">Se connecter</a>
{% endif %} {% endif %}
<a href="{% url 'download' %}" class="download-button">Ajouter vos tournois</a> <a href="{% url 'download' %}" class="download-button">Ajouter vos tournois</a>
<a href="{% url 'shop:product_list' %}">La Boutique</a>
</nav> </nav>

Loading…
Cancel
Save