furry/products/models.py

429 lines
16 KiB
Python

from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db.models.signals import post_save
from django.dispatch import receiver
from shop.models import Category # Import Category aus der shop App
from typing import Optional, List
from decimal import Decimal
# Create your models here.
class UserProfile(models.Model):
"""Erweitertes Benutzerprofil mit zusätzlichen Informationen"""
user: User = models.OneToOneField(User, on_delete=models.CASCADE)
phone: str = models.CharField(max_length=20, blank=True)
address: str = models.TextField(blank=True)
default_shipping_address: str = models.TextField(blank=True)
newsletter: bool = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return f"Profil von {self.user.username}"
class Meta:
verbose_name = 'Benutzerprofil'
verbose_name_plural = 'Benutzerprofile'
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
class Product(models.Model):
FURSUIT_TYPE_CHOICES = [
('partial', 'Partial'),
('fullsuit', 'Fullsuit'),
('head', 'Head Only'),
('paws', 'Paws'),
('tail', 'Tail'),
('other', 'Other'),
]
STYLE_CHOICES = [
('toony', 'Toony'),
('semi_realistic', 'Semi-Realistic'),
('realistic', 'Realistic'),
('anime', 'Anime'),
('chibi', 'Chibi'),
]
name = models.CharField(max_length=200)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
stock = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
image = models.ImageField(upload_to='products/', null=True, blank=True)
# Neue Felder
fursuit_type = models.CharField(max_length=20, choices=FURSUIT_TYPE_CHOICES, default='other')
style = models.CharField(max_length=20, choices=STYLE_CHOICES, default='toony')
is_featured = models.BooleanField(default=False)
is_custom_order = models.BooleanField(default=False)
# Beziehungen
category = models.ForeignKey('shop.Category', on_delete=models.SET_NULL, null=True, blank=True, related_name='product_items', verbose_name='Kategorie')
wishlist_users = models.ManyToManyField(User, related_name='wishlist_products', blank=True)
def __str__(self):
return self.name
def average_rating(self):
if self.reviews.exists():
return self.reviews.aggregate(models.Avg('rating'))['rating__avg']
return 0
class Meta:
ordering = ['-created']
indexes = [
models.Index(fields=['name']),
models.Index(fields=['price']),
models.Index(fields=['created']),
models.Index(fields=['fursuit_type']),
models.Index(fields=['style']),
models.Index(fields=['is_featured']),
models.Index(fields=['stock']),
models.Index(fields=['category', 'fursuit_type']),
models.Index(fields=['price', 'fursuit_type']),
]
class Review(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews')
user = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.IntegerField(validators=[MinValueValidator(1)])
comment = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('product', 'user')
ordering = ['-created']
def __str__(self):
return f'Review by {self.user.username} for {self.product.name}'
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='product_carts')
session_id = models.CharField(max_length=100, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def get_total(self):
return sum(item.get_subtotal() for item in self.items.all())
def __str__(self):
return f"Cart {self.id} - {'User: ' + self.user.username if self.user else 'Session: ' + self.session_id}"
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
created = models.DateTimeField(auto_now_add=True)
def get_subtotal(self):
return self.product.price * self.quantity
def __str__(self):
return f"{self.quantity}x {self.product.name} in Cart {self.cart.id}"
class Meta:
unique_together = ('cart', 'product')
class Order(models.Model):
STATUS_CHOICES = [
('pending', 'Ausstehend'),
('processing', 'In Bearbeitung'),
('shipped', 'Versendet'),
('delivered', 'Geliefert'),
('cancelled', 'Storniert'),
]
PAYMENT_STATUS_CHOICES = [
('pending', 'Ausstehend'),
('processing', 'Wird bearbeitet'),
('paid', 'Bezahlt'),
('failed', 'Fehlgeschlagen'),
('refunded', 'Zurückerstattet'),
]
PAYMENT_METHOD_CHOICES = [
('card', 'Kreditkarte'),
('sepa', 'SEPA-Lastschrift'),
('giropay', 'Giropay'),
('sofort', 'Sofort'),
('bancontact', 'Bancontact'),
]
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='product_orders')
full_name = models.CharField(max_length=200)
email = models.EmailField()
address = models.TextField()
phone = models.CharField(max_length=20)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
# Zahlungsinformationen
payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending')
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHOD_CHOICES, null=True, blank=True)
stripe_payment_intent_id = models.CharField(max_length=100, null=True, blank=True)
stripe_payment_method_id = models.CharField(max_length=100, null=True, blank=True)
payment_date = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"Bestellung #{self.id} von {self.full_name}"
class Meta:
ordering = ['-created']
indexes = [
models.Index(fields=['status']),
models.Index(fields=['payment_status']),
models.Index(fields=['created']),
]
def get_payment_status_display_class(self):
status_classes = {
'pending': 'warning',
'processing': 'info',
'paid': 'success',
'failed': 'danger',
'refunded': 'secondary',
}
return status_classes.get(self.payment_status, 'secondary')
def get_order_status_display_class(self):
status_classes = {
'pending': 'warning',
'processing': 'info',
'shipped': 'primary',
'delivered': 'success',
'cancelled': 'danger',
}
return status_classes.get(self.status, 'secondary')
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
product_name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField()
def get_subtotal(self):
return self.price * self.quantity
def __str__(self):
return f"{self.quantity}x {self.product_name} in Bestellung #{self.order.id}"
class Wishlist(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
products = models.ManyToManyField(Product)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Wunschliste von {self.user.username}"
class FAQ(models.Model):
question = models.CharField(max_length=255, verbose_name='Frage')
answer = models.TextField(verbose_name='Antwort')
category = models.CharField(max_length=100, verbose_name='Kategorie')
order = models.IntegerField(default=0, verbose_name='Reihenfolge')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['category', 'order']
verbose_name = 'FAQ'
verbose_name_plural = 'FAQs'
def __str__(self):
return self.question
class ContactMessage(models.Model):
CATEGORY_CHOICES = [
('general', 'Allgemeine Anfrage'),
('order', 'Bestellung'),
('return', 'Rückgabe/Umtausch'),
('complaint', 'Beschwerde'),
('technical', 'Technische Frage'),
]
name = models.CharField(max_length=100)
email = models.EmailField()
order_number = models.CharField(max_length=50, blank=True, null=True)
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
subject = models.CharField(max_length=200)
message = models.TextField()
created = models.DateTimeField(auto_now_add=True)
status = models.CharField(
max_length=20,
choices=[
('new', 'Neu'),
('in_progress', 'In Bearbeitung'),
('resolved', 'Erledigt'),
('closed', 'Geschlossen'),
],
default='new'
)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
staff_notes = models.TextField(blank=True)
class Meta:
ordering = ['-created']
verbose_name = 'Kontaktanfrage'
verbose_name_plural = 'Kontaktanfragen'
def __str__(self):
return f"{self.category} - {self.subject} ({self.created.strftime('%d.%m.%Y')})"
class CustomOrder(models.Model):
STATUS_CHOICES = [
('pending', 'Anfrage eingegangen'),
('quoted', 'Angebot erstellt'),
('approved', 'Angebot akzeptiert'),
('in_progress', 'In Arbeit'),
('ready', 'Fertig zur Abholung'),
('shipped', 'Versendet'),
('completed', 'Abgeschlossen'),
('cancelled', 'Storniert'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE)
fursuit_type = models.CharField(max_length=20, choices=Product.FURSUIT_TYPE_CHOICES)
style = models.CharField(max_length=20, choices=Product.STYLE_CHOICES)
character_name = models.CharField(max_length=100)
character_description = models.TextField()
reference_images = models.FileField(upload_to='references/', null=True, blank=True)
special_requests = models.TextField(blank=True)
measurements = models.TextField()
color_preferences = models.TextField()
budget_range = models.CharField(max_length=100)
deadline_request = models.DateField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
quoted_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Custom Order #{self.id} - {self.character_name}"
class Meta:
ordering = ['-created']
class OrderProgress(models.Model):
PROGRESS_CHOICES = [
('design', 'Design & Planung'),
('base', 'Grundform'),
('fur', 'Fell'),
('details', 'Details'),
('electronics', 'Elektronik (optional)'),
('finishing', 'Finishing'),
]
custom_order = models.ForeignKey(CustomOrder, on_delete=models.CASCADE, related_name='progress_updates')
stage = models.CharField(max_length=20, choices=PROGRESS_CHOICES)
description = models.TextField()
image = models.ImageField(upload_to='progress/', null=True, blank=True)
completed = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.custom_order.character_name} - {self.get_stage_display()}"
class Meta:
ordering = ['created']
class GalleryImage(models.Model):
FURSUIT_TYPE_CHOICES = [
('full', 'Fullsuit'),
('partial', 'Partial'),
('head', 'Head Only'),
('other', 'Other'),
]
STYLE_CHOICES = [
('toony', 'Toony'),
('semi', 'Semi-Realistic'),
('real', 'Realistic'),
('anime', 'Anime'),
]
title = models.CharField(max_length=200, verbose_name='Titel')
description = models.TextField(blank=True, verbose_name='Beschreibung')
image = models.ImageField(upload_to='gallery/', verbose_name='Bild')
fursuit_type = models.CharField(
max_length=20,
choices=FURSUIT_TYPE_CHOICES,
default='full',
verbose_name='Fursuit-Typ'
)
style = models.CharField(
max_length=20,
choices=STYLE_CHOICES,
default='toony',
verbose_name='Stil'
)
is_featured = models.BooleanField(default=False, verbose_name='Hervorgehoben')
order = models.IntegerField(default=0, verbose_name='Reihenfolge')
created = models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')
class Meta:
verbose_name = 'Galeriebild'
verbose_name_plural = 'Galeriebilder'
ordering = ['order', '-created']
def __str__(self):
return self.title
class Payment(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
variant = models.CharField(max_length=255)
status = models.CharField(max_length=10)
fraud_status = models.CharField(max_length=10, null=True, blank=True)
fraud_message = models.TextField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
transaction_id = models.CharField(max_length=255, null=True, blank=True)
currency = models.CharField(max_length=10)
total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
delivery = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
description = models.TextField(null=True, blank=True)
billing_first_name = models.CharField(max_length=256, null=True, blank=True)
billing_last_name = models.CharField(max_length=256, null=True, blank=True)
billing_address_1 = models.CharField(max_length=256, null=True, blank=True)
billing_address_2 = models.CharField(max_length=256, null=True, blank=True)
billing_city = models.CharField(max_length=256, null=True, blank=True)
billing_postcode = models.CharField(max_length=256, null=True, blank=True)
billing_country_code = models.CharField(max_length=2, null=True, blank=True)
billing_country_area = models.CharField(max_length=256, null=True, blank=True)
billing_email = models.EmailField(null=True, blank=True)
customer_ip_address = models.GenericIPAddressField(null=True, blank=True)
extra_data = models.TextField(null=True, blank=True)
message = models.TextField(null=True, blank=True)
token = models.CharField(max_length=36, null=True, blank=True)
captured_amount = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
class Meta:
ordering = ['-created']
def __str__(self):
return f"Payment {self.id} for Order {self.order.id}"
@property
def attrs(self):
return {}
def get_failure_url(self):
return f'/payment/failed/{self.order.id}/'
def get_success_url(self):
return f'/payment/success/{self.order.id}/'