Removed per-product currency in favor of a global site currency managed via SiteConfiguration. Updated models, views, templates, and Stripe integration to use the global currency. Added migration, management command for migration, and API endpoint for currency info. Improved permissions and filtering for orders, reviews, and carts. Expanded supported currencies in configuration.
133 lines
5.8 KiB
Python
133 lines
5.8 KiB
Python
import decimal
|
|
from django.db import models
|
|
from decimal import Decimal
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
|
|
# Create your models here.
|
|
|
|
class SiteConfiguration(models.Model):
|
|
name = models.CharField(max_length=100, default="Shop name", unique=True)
|
|
|
|
logo = models.ImageField(upload_to='shop_logos/', blank=True, null=True)
|
|
favicon = models.ImageField(upload_to='shop_favicons/', blank=True, null=True)
|
|
|
|
contact_email = models.EmailField(max_length=254, blank=True, null=True)
|
|
contact_phone = models.CharField(max_length=20, blank=True, null=True)
|
|
contact_address = models.TextField(blank=True, null=True)
|
|
opening_hours = models.JSONField(blank=True, null=True) #FIXME: vytvoř JSON tvar pro otvírací dobu, přes validátory
|
|
|
|
#Social
|
|
facebook_url = models.URLField(max_length=200, blank=True, null=True)
|
|
instagram_url = models.URLField(max_length=200, blank=True, null=True)
|
|
youtube_url = models.URLField(max_length=200, blank=True, null=True)
|
|
tiktok_url = models.URLField(max_length=200, blank=True, null=True)
|
|
whatsapp_number = models.CharField(max_length=20, blank=True, null=True)
|
|
|
|
#zasilkovna settings
|
|
zasilkovna_shipping_price = models.DecimalField(max_digits=10, decimal_places=2, default=decimal.Decimal("50.00"))
|
|
#FIXME: není implementováno ↓↓↓
|
|
zasilkovna_api_key = models.CharField(max_length=255, blank=True, null=True, help_text="API klíč pro přístup k Zásilkovna API (zatím není využito)")
|
|
#FIXME: není implementováno ↓↓↓
|
|
zasilkovna_api_password = models.CharField(max_length=255, blank=True, null=True, help_text="API heslo pro přístup k Zásilkovna API (zatím není využito)")
|
|
#FIXME: není implementováno ↓↓↓
|
|
free_shipping_over = models.DecimalField(max_digits=10, decimal_places=2, default=decimal.Decimal("2000.00"))
|
|
|
|
# Deutsche Post settings
|
|
deutschepost_api_url = models.URLField(max_length=255, default="https://gw.sandbox.deutschepost.com", help_text="Deutsche Post API URL (sandbox/production)")
|
|
deutschepost_client_id = models.CharField(max_length=255, blank=True, null=True, help_text="Deutsche Post OAuth Client ID")
|
|
deutschepost_client_secret = models.CharField(max_length=255, blank=True, null=True, help_text="Deutsche Post OAuth Client Secret")
|
|
deutschepost_customer_ekp = models.CharField(max_length=20, blank=True, null=True, help_text="Deutsche Post Customer EKP number")
|
|
deutschepost_shipping_price = models.DecimalField(max_digits=10, decimal_places=2, default=decimal.Decimal("6.00"), help_text="Default Deutsche Post shipping price in EUR")
|
|
|
|
#coupon settings
|
|
multiplying_coupons = models.BooleanField(default=True, help_text="Násobení kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón")
|
|
addition_of_coupons_amount = models.BooleanField(default=False, help_text="Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón")
|
|
|
|
class CURRENCY(models.TextChoices):
|
|
EUR = "EUR", "Euro"
|
|
CZK = "CZK", "Czech Koruna"
|
|
USD = "USD", "US Dollar"
|
|
GBP = "GBP", "British Pound"
|
|
PLN = "PLN", "Polish Zloty"
|
|
HUF = "HUF", "Hungarian Forint"
|
|
SEK = "SEK", "Swedish Krona"
|
|
DKK = "DKK", "Danish Krone"
|
|
NOK = "NOK", "Norwegian Krone"
|
|
CHF = "CHF", "Swiss Franc"
|
|
currency = models.CharField(max_length=10, default=CURRENCY.EUR, choices=CURRENCY.choices)
|
|
|
|
class Meta:
|
|
verbose_name = "Shop Configuration"
|
|
verbose_name_plural = "Shop Configuration"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# zajištění singletonu
|
|
self.pk = 1
|
|
super().save(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def get_solo(cls):
|
|
obj, _ = cls.objects.get_or_create(pk=1)
|
|
return obj
|
|
|
|
|
|
class VATRate(models.Model):
|
|
"""Business owner configurable VAT rates"""
|
|
name = models.CharField(
|
|
max_length=100,
|
|
help_text="E.g. 'German Standard', 'German Reduced', 'Czech Standard'"
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
help_text="Optional description: 'Standard rate for most products', 'Books and food', etc."
|
|
)
|
|
|
|
rate = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=4, # Allows rates like 19.5000%
|
|
validators=[MinValueValidator(Decimal('0')), MaxValueValidator(Decimal('100'))],
|
|
help_text="VAT rate as percentage (e.g. 19.00 for 19%)"
|
|
)
|
|
|
|
|
|
|
|
is_default = models.BooleanField(
|
|
default=False,
|
|
help_text="Default rate for new products"
|
|
)
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text="Whether this VAT rate is active and available for use"
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
verbose_name = "VAT Rate"
|
|
verbose_name_plural = "VAT Rates"
|
|
ordering = ['-is_default', 'rate', 'name']
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.rate}%)"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Ensure only one default rate
|
|
if self.is_default:
|
|
VATRate.objects.filter(is_default=True).update(is_default=False)
|
|
super().save(*args, **kwargs)
|
|
|
|
# If no default exists, make first active one default
|
|
if not VATRate.objects.filter(is_default=True).exists():
|
|
first_active = VATRate.objects.filter(is_active=True).first()
|
|
if first_active:
|
|
first_active.is_default = True
|
|
first_active.save()
|
|
|
|
@property
|
|
def rate_decimal(self):
|
|
"""Returns rate as decimal for calculations (19.00% -> 0.19)"""
|
|
return self.rate / Decimal('100')
|
|
|
|
@classmethod
|
|
def get_default(cls):
|
|
"""Get the default VAT rate"""
|
|
return cls.objects.filter(is_default=True, is_active=True).first() |