215 lines
7.5 KiB
Python
215 lines
7.5 KiB
Python
from django.db import models
|
||
from django.conf import settings
|
||
from django.utils import timezone
|
||
from django.utils.translation import gettext_lazy as _
|
||
from decimal import Decimal
|
||
|
||
|
||
class Category(models.Model):
|
||
name = models.CharField(max_length=100)
|
||
|
||
#adresa kategorie např: /category/elektronika/mobily/
|
||
url = models.SlugField(unique=True)
|
||
|
||
#kategorie se můžou skládat pod sebe
|
||
parent = models.ForeignKey(
|
||
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='subcategories'
|
||
)
|
||
|
||
description = models.TextField(blank=True)
|
||
|
||
#ikona
|
||
image = models.ImageField(upload_to='categories/', blank=True)
|
||
|
||
class Meta:
|
||
verbose_name_plural = "Categories"
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
|
||
class Product(models.Model):
|
||
name = models.CharField(max_length=200)
|
||
description = models.TextField(blank=True)
|
||
code = models.CharField(max_length=100, unique=True, blank=True, null=True)
|
||
|
||
category = models.ForeignKey(Category, related_name='products', on_delete=models.PROTECT)
|
||
|
||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||
currency = models.CharField(max_length=10, default="czk")
|
||
|
||
url = models.SlugField(unique=True)
|
||
|
||
stock = models.PositiveIntegerField(default=0)
|
||
is_active = models.BooleanField(default=True)
|
||
|
||
#limitka
|
||
limited_to = models.DateTimeField(null=True, blank=True)
|
||
|
||
default_carrier = models.ForeignKey(
|
||
"Carrier", on_delete=models.SET_NULL, null=True, blank=True, related_name="default_for_products"
|
||
)
|
||
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
@property
|
||
def available(self):
|
||
return self.is_active and self.stock > 0
|
||
|
||
def __str__(self):
|
||
return f"{self.name} ({self.price} {self.currency.upper()})"
|
||
|
||
#obrázek pro produkty
|
||
class ProductImage(models.Model):
|
||
product = models.ForeignKey(Product, related_name='images', on_delete=models.CASCADE)
|
||
|
||
image = models.ImageField(upload_to='products/')
|
||
|
||
alt_text = models.CharField(max_length=150, blank=True)
|
||
is_main = models.BooleanField(default=False)
|
||
|
||
def __str__(self):
|
||
return f"{self.product.name} image"
|
||
|
||
|
||
# Dopravci a způsoby dopravy
|
||
class Carrier(models.Model):
|
||
name = models.CharField(max_length=100) # název dopravce (Zásilkovna, Česká pošta…)
|
||
base_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) # základní cena dopravy
|
||
delivery_time = models.CharField(max_length=100, blank=True) # např. "2–3 pracovní dny"
|
||
is_active = models.BooleanField(default=True)
|
||
|
||
# pole pro logo
|
||
logo = models.ImageField(upload_to="carriers/", blank=True, null=True)
|
||
|
||
# pole pro propojení s externím API (např. ID služby u Zásilkovny)
|
||
external_id = models.CharField(max_length=50, blank=True, null=True)
|
||
|
||
def __str__(self):
|
||
return f"{self.name} ({self.base_price} Kč)"
|
||
|
||
|
||
|
||
class DiscountCode(models.Model):
|
||
code = models.CharField(max_length=50, unique=True)
|
||
description = models.CharField(max_length=255, blank=True)
|
||
|
||
# sleva v procentech (0–100)
|
||
percent = models.DecimalField(max_digits=5, decimal_places=2, help_text="Např. 10.00 = 10% sleva")
|
||
|
||
# nebo fixní částka
|
||
amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text="Fixní sleva v CZK")
|
||
|
||
valid_from = models.DateTimeField(default=timezone.now)
|
||
valid_to = models.DateTimeField(null=True, blank=True)
|
||
active = models.BooleanField(default=True)
|
||
|
||
usage_limit = models.PositiveIntegerField(null=True, blank=True)
|
||
used_count = models.PositiveIntegerField(default=0)
|
||
|
||
specific_products = models.ManyToManyField(
|
||
Product, blank=True, related_name="discount_codes"
|
||
)
|
||
specific_categories = models.ManyToManyField(
|
||
Category, blank=True, related_name="discount_codes"
|
||
)
|
||
|
||
def is_valid(self):
|
||
now = timezone.now()
|
||
if not self.active:
|
||
return False
|
||
if self.valid_to and self.valid_to < now:
|
||
return False
|
||
if self.usage_limit and self.used_count >= self.usage_limit:
|
||
return False
|
||
return True
|
||
|
||
def __str__(self):
|
||
return f"{self.code} ({self.percent}% or {self.amount} CZK)"
|
||
|
||
|
||
class Order(models.Model):
|
||
class Status(models.TextChoices):
|
||
PENDING = "pending", _("Čeká na platbu")
|
||
PAID = "paid", _("Zaplaceno")
|
||
CANCELLED = "cancelled", _("Zrušeno")
|
||
SHIPPED = "shipped", _("Odesláno")
|
||
COMPLETED = "completed", _("Dokončeno")
|
||
|
||
user = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL, related_name="orders", on_delete=models.CASCADE
|
||
)
|
||
|
||
status = models.CharField(
|
||
max_length=20, choices=Status.choices, default=Status.PENDING
|
||
)
|
||
|
||
# Stored order grand total; recalculated on save
|
||
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||
currency = models.CharField(max_length=10, default="CZK")
|
||
|
||
# fakturační údaje (zkopírované z user profilu při objednávce)
|
||
first_name = models.CharField(max_length=100)
|
||
last_name = models.CharField(max_length=100)
|
||
email = models.EmailField()
|
||
phone = models.CharField(max_length=20, blank=True)
|
||
address = models.CharField(max_length=255)
|
||
city = models.CharField(max_length=100)
|
||
postal_code = models.CharField(max_length=20)
|
||
country = models.CharField(max_length=100, default="Czech Republic")
|
||
|
||
note = models.TextField(blank=True)
|
||
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
discount = models.ManyToOneRel("DiscountCode", null=True, blank=True, on_delete=models.PROTECT)
|
||
|
||
def __str__(self):
|
||
return f"Order #{self.id} - {self.user.email} ({self.status})"
|
||
|
||
def calculate_total_price(self):
|
||
for discount in self.discount.all():
|
||
total = Decimal('0.0')
|
||
|
||
# getting all prices from order items (with discount applied if valid)
|
||
for item in self.items.all():
|
||
total = total + item.get_total_price(discount)
|
||
|
||
return total
|
||
|
||
|
||
def save(self, *args, **kwargs):
|
||
|
||
# Keep total_price always in sync with items and discount
|
||
self.total_price = self.calculate_total_price()
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
class OrderItem(models.Model):
|
||
order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE)
|
||
product = models.ForeignKey("products.Product", on_delete=models.PROTECT)
|
||
quantity = models.PositiveIntegerField(default=1)
|
||
|
||
def get_total_price(self, discount_object:DiscountCode):
|
||
"""Calculate total price for this item, applying discount if valid."""
|
||
|
||
if discount_object and discount_object.is_valid():
|
||
|
||
if (self.product in discount_object.specific_products.all() or self.product.category in discount_object.specific_categories.all()):
|
||
if discount_object.percent:
|
||
return (self.quantity * self.product.price) * (Decimal('1.0') - discount_object.percent / Decimal('100'))
|
||
|
||
elif discount_object.amount:
|
||
return (self.quantity * self.product.price) - discount_object.amount
|
||
|
||
else:
|
||
raise ValueError("Discount code must have either percent or amount defined.")
|
||
else:
|
||
return ValueError("Invalid discount code.")
|
||
|
||
def __str__(self):
|
||
return f"{self.product.name} x{self.quantity}" |