from django.db import models from django.conf import settings from django.utils import timezone class OrderStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' PAID = 'PAID', 'Paid' SHIPPED = 'SHIPPED', 'Shipped' DELIVERED = 'DELIVERED', 'Delivered' CANCELED = 'CANCELED', 'Canceled' REFUNDED = 'REFUNDED', 'Refunded' PREPARED = 'PREPARED', 'Prepared' READY = 'READY', 'Ready' class CutChoices(models.IntegerChoices): UNISEX = 0, 'Unisex' WOMEN = 1, 'Women' MEN = 2, 'Men' KIDS = 3, 'Kids' class Color(models.Model): name = models.CharField(max_length=40, unique=True) colorHex = models.CharField(max_length=7, default="#FFFFFF", help_text="Color in hex format (e.g. #FF0000)") secondary_hex_color = models.CharField(max_length=7, null=True, blank=True, help_text="Secondary color in hex format for split color display") ordering = models.IntegerField(default=0) class Meta: ordering = ['ordering'] # This will make queries respect the ordering by default def __str__(self): return self.name class Size(models.Model): name = models.CharField(max_length=20, unique=True) def __str__(self): return self.name class Product(models.Model): sku = models.CharField(max_length=50, unique=True, help_text="Product SKU (unique identifier)") title = models.CharField(max_length=200) description = models.TextField(blank=True, null=True, help_text="Product description text") image = models.CharField(max_length=200, null=True, blank=True) price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) # 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") ordering_value = models.IntegerField(default=0, blank=False) cut = models.IntegerField(choices=CutChoices.choices, default=CutChoices.UNISEX) class Meta: ordering = ['ordering_value', 'cut'] # Add this line to sort by title def __str__(self): return f"{self.sku} - {self.title}" class CartItem(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) 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) session_id = models.CharField(max_length=255, null=True, blank=True) date_added = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['product__ordering_value', 'product__cut'] # Sort by product's ordering_value def __str__(self): return f"{self.quantity} x {self.product.title}" def get_total_price(self): 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): email = models.EmailField() phone = models.CharField(max_length=20) def __str__(self): return f"{self.email}" class Coupon(models.Model): code = models.CharField(max_length=50, unique=True) description = models.CharField(max_length=255, blank=True) discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) discount_percent = models.DecimalField(max_digits=5, decimal_places=2, default=0) is_active = models.BooleanField(default=True) valid_from = models.DateTimeField() valid_to = models.DateTimeField() max_uses = models.IntegerField(default=0) # 0 for unlimited current_uses = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) stripe_coupon_id = models.CharField(max_length=100, blank=True, null=True) def __str__(self): return self.code def is_valid(self): now = timezone.now() if not self.is_active: return False if now < self.valid_from or now > self.valid_to: return False if self.max_uses > 0 and self.current_uses >= self.max_uses: return False return True def get_discount_amount_for_total(self, total_price): """Calculate the discount amount for a given total price""" if self.discount_percent > 0: return (self.discount_percent / 100) * total_price return self.discount_amount class Order(models.Model): 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) 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) stripe_payment_intent_id = models.CharField(max_length=255, blank=True, null=True) stripe_checkout_session_id = models.CharField(max_length=255, blank=True, null=True) payment_status = models.CharField(max_length=20, default='UNPAID', choices=[ ('UNPAID', 'Unpaid'), ('PAID', 'Paid'), ('FAILED', 'Failed'), ('REFUNDED', 'Refunded') ]) webhook_processed = models.BooleanField(default=False) stripe_mode = models.CharField(max_length=10, default='test', choices=[ ('test', 'Test Mode'), ('live', 'Live Mode'), ]) coupon = models.ForeignKey(Coupon, on_delete=models.SET_NULL, null=True, blank=True) discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) def __str__(self): return f"Order #{self.id} - {self.status}" def get_total_after_discount(self): 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, OrderStatus.READY] def get_shipping_address(self): """ Returns a formatted string of the shipping address """ if not self.shipping_address: return "Aucune adresse de livraison fournie" address_parts = [ self.shipping_address.street_address, self.shipping_address.apartment if self.shipping_address.apartment else None, self.shipping_address.city, self.shipping_address.state if self.shipping_address.state else None, self.shipping_address.postal_code, self.shipping_address.country ] # Filter out None values and join with newlines formatted_address = '\n'.join(part for part in address_parts if part) return formatted_address 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) class Meta: ordering = ['product__ordering_value', 'product__cut'] # Sort by product's ordering_value def __str__(self): return f"{self.quantity} x {self.product.title}" def get_total_price(self): return self.price * self.quantity class CouponUsage(models.Model): coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name='usages') user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='coupon_usages') guest_email = models.EmailField(blank=True, null=True) used_at = models.DateTimeField(auto_now_add=True) def __str__(self): user_identifier = self.user.username if self.user else self.guest_email return f"{self.coupon.code} - {user_identifier} - {self.used_at}"