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 from configuration.models import ShopConfiguration from thirdparty.zasilkovna.models import ZasilkovnaPacket from thirdparty.stripe.models import StripePayment #FIXME: přidat soft delete pro všchny modely !!!! 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) url = models.SlugField(unique=True) stock = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True) #limitka (volitelné) 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" # ------------------ OBJEDNÁVKY ------------------ 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") 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) FIXME: rozhodnout se co dát do on_delete user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="orders", null=True, blank=True ) 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) carrier = models.OneToOneField( "Carrier", on_delete=models.CASCADE, related_name="orders", null=True, blank=True ) payment = models.OneToOneField( "Payment", on_delete=models.CASCADE, related_name="orders", null=True, blank=True ) discount = models.ManyToManyField("DiscountCode", blank=True, related_name="orders") def calculate_total_price(self): carrier_price = self.carrier.get_price() if self.carrier else Decimal("0.0") if self.discount.exists(): 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 + carrier_price else: total = Decimal('0.0') # getting all prices from order items (without discount) for item in self.items.all(): total = total + (item.product.price * item.quantity) return total + carrier_price def import_data_from_user(self): """Import user data into order for billing purposes.""" self.first_name = self.user.first_name self.last_name = self.user.last_name self.email = self.user.email self.phone = self.user.phone self.address = f"{self.user.street} {self.user.street_number}" self.city = self.user.city self.postal_code = self.user.postal_code self.country = self.user.country def save(self, *args, **kwargs): # Keep total_price always in sync with items and discount self.total_price = self.calculate_total_price() if self.user and self.pk is None: self.import_data_from_user() super().save(*args, **kwargs) # ------------------ DOPRAVCI A ZPŮSOBY DOPRAVY ------------------ class Carrier(models.Model): class SHIPPING(models.TextChoices): ZASILKOVNA = "packeta", "Zásilkovna" STORE = "store", "Osobní odběr" shipping_method = models.CharField(max_length=20, choices=SHIPPING.choices, default=SHIPPING.STORE) # prodejce to přidá později zasilkovna = models.ForeignKey( ZasilkovnaPacket, on_delete=models.DO_NOTHING, null=True, blank=True, related_name="carriers" ) weight = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text="Hmotnost zásilky v kg") returning = models.BooleanField(default=False, help_text="Zda je tato zásilka na vrácení") def save(self, *args, **kwargs): super().save(*args, **kwargs) def get_price(self): if self.shipping_method == self.SHIPPING.ZASILKOVNA: return ShopConfiguration.get_solo().zasilkovna_shipping_price else: return Decimal('0.0') #tohle bude vyvoláno pomocí admina přes api!!! def start_ordering_shipping(self): if self.shipping_method == self.SHIPPING.ZASILKOVNA: #už při vytvoření se volá na api Zásilkovny self.zasilkovna = ZasilkovnaPacket.objects.create() #... další logika pro jiné způsoby dopravy #TODO: přidat notifikace uživateli, jak pro zásilkovnu, tak pro vyzvednutí v obchodě! def returning_shipping(self): self.returning = True if self.shipping_method == self.SHIPPING.ZASILKOVNA: #volá se na api Zásilkovny self.zasilkovna.returning_packet() # ------------------ PLATEBNÍ MODELY ------------------ class Payment(models.Model): class PAYMENT(models.TextChoices): SHOP = "shop", "Platba v obchodě" STRIPE = "stripe", "Bankovní převod" CASH_ON_DELIVERY = "cash_on_delivery", "Dobírka" payment_method = models.CharField(max_length=30, choices=PAYMENT.choices, default=PAYMENT.SHOP) #active = models.BooleanField(default=True) #FIXME: potvrdit že logika platby funguje správně #veškera logika a interakce bude na stripu (třeba aktualizovaní objednávky na zaplacenou apod.) stripe = models.OneToOneField( StripePayment, on_delete=models.CASCADE, null=True, blank=True, related_name="payment" ) def save(self, *args, **kwargs): order = Order.objects.get(payment=self) if self.payment_method == self.PAYMENT.SHOP and Carrier.objects.get(orders=order).shipping_method != Carrier.SHIPPING.STORE: raise ValueError("Platba v obchodě je možná pouze pro osobní odběr.") super().save(*args, **kwargs) # ------------------ SLEVOVÉ KÓDY ------------------ 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)" # ------------------ OBJEDNANÉ POLOŽKY ------------------ 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 = None): #FIXME: přidat logiku pro slevové kódy """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.") elif not discount_object: return self.quantity * self.product.price else: raise ValueError("Invalid discount code.") def __str__(self): return f"{self.product.name} x{self.quantity}"