417 lines
16 KiB
Python
417 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
|
|
|
|
# Create your models here.
|
|
|
|
class UserProfile(models.Model):
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
|
phone = models.CharField(max_length=20, blank=True)
|
|
address = models.TextField(blank=True)
|
|
default_shipping_address = models.TextField(blank=True)
|
|
newsletter = models.BooleanField(default=False)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
updated = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f"Profil von {self.user.username}"
|
|
|
|
@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']),
|
|
]
|
|
|
|
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}/'
|