Files
vontor-cz/backend/commerce/models.py
Brunobrno f14c09bf7a Refactor commerce models and add configuration app
Major refactor of commerce models: restructured Carrier, Payment, and DiscountCode models, improved order total calculation, and integrated Zasilkovna and Stripe logic. Added new configuration Django app for shop settings, updated Zasilkovna and Stripe models, and fixed Zasilkovna client WSDL URL. Removed unused serializers and views in commerce, and registered new apps in settings.
2025-11-14 02:21:20 +01:00

321 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (0100)
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}"