Introduced a full-featured analytics module for e-commerce business intelligence, including sales, product, customer, shipping, and review analytics, with API endpoints for dashboard and custom reports. Added VAT rate management: new VATRate model, admin interface, serializer, and API endpoints, and integrated VAT logic into Product and pricing calculations. Refactored configuration and admin code to support VAT rates, improved email notification tasks, and updated related serializers, views, and URLs for unified configuration and VAT management.
121 lines
5.4 KiB
Python
121 lines
5.4 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):
|
|
CZK = "CZK", "cz#Czech Koruna"
|
|
EUR = "EUR", "cz#Euro"
|
|
currency = models.CharField(max_length=10, default=CURRENCY.CZK, 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"
|
|
)
|
|
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() |