From 5c3a02d28218ea83086c47035512d0f45880ff42 Mon Sep 17 00:00:00 2001 From: Brunobrno Date: Thu, 13 Nov 2025 02:32:56 +0100 Subject: [PATCH] commerce --- backend/account/views.py | 2 +- backend/commerce/models.py | 79 ++++++++++++++----- backend/thirdparty/zasilkovna/__init__.py | 0 backend/thirdparty/zasilkovna/admin.py | 3 + backend/thirdparty/zasilkovna/apps.py | 6 ++ .../zasilkovna/migrations/__init__.py | 0 backend/thirdparty/zasilkovna/models.py | 18 +++++ backend/thirdparty/zasilkovna/tests.py | 3 + backend/thirdparty/zasilkovna/views.py | 4 + frontend/index.html | 30 ++++--- 10 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 backend/thirdparty/zasilkovna/__init__.py create mode 100644 backend/thirdparty/zasilkovna/admin.py create mode 100644 backend/thirdparty/zasilkovna/apps.py create mode 100644 backend/thirdparty/zasilkovna/migrations/__init__.py create mode 100644 backend/thirdparty/zasilkovna/models.py create mode 100644 backend/thirdparty/zasilkovna/tests.py create mode 100644 backend/thirdparty/zasilkovna/views.py diff --git a/backend/account/views.py b/backend/account/views.py index 8476edd..4660148 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -6,7 +6,7 @@ from .serializers import * from .permissions import * from .models import CustomUser from .tokens import * -from .tasks import send_password_reset_email_task +from .tasks import send_password_reset_email_task, send_email_verification_task, send_email_clerk_accepted_task from django.conf import settings import logging logger = logging.getLogger(__name__) diff --git a/backend/commerce/models.py b/backend/commerce/models.py index 3b63bf1..19343a7 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -4,6 +4,8 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from decimal import Decimal +from thirdparty.zasilkovna.models import PacketaShipment + class Category(models.Model): name = models.CharField(max_length=100) @@ -32,19 +34,20 @@ class Category(models.Model): 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 + #limitka (volitelné) limited_to = models.DateTimeField(null=True, blank=True) default_carrier = models.ForeignKey( @@ -74,21 +77,36 @@ class ProductImage(models.Model): return f"{self.product.name} image" +# ------------------ OBJENDÁVKOVÉ MODELY (dole) ------------------ + + # 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) + class Role(models.TextChoices): + ZASILKOVNA = "packeta", "Zásilkovna" + STORE = "store", "Osobní odběr" - # 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) + choice = models.CharField(max_length=20, choices=Role.choices, default=Role.STORE) + + + zasilkovna = models.ForeignKey( + 'thirdparty.zasilkovna.Zasilkovna', on_delete=models.DO_NOTHING, null=True, blank=True, related_name="carriers" + ) def __str__(self): return f"{self.name} ({self.base_price} Kč)" + + def save(self, *args, **kwargs): + + #zásilkovna instance je vytvořena + if self.choice == self.Role.ZASILKOVNA: + self.packeta = PacketaShipment.objects.create() + + elif self.choice == self.Role.STORE: + self.packeta = None + + super().save(*args, **kwargs) @@ -130,20 +148,30 @@ class DiscountCode(models.Model): 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") + #COMPLETED = "completed", _("Dokončeno") + + status = models.CharField( + max_length=20, choices=Status.choices, default=Status.PENDING + ) 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 + carrier = models.ForeignKey( + Carrier, on_delete=models.CASCADE, null=True, blank=True, related_name="orders" + ) + + #itemy + order_items = models.ManyToManyField( + 'OrderItem', related_name='orders', ) # Stored order grand total; recalculated on save @@ -165,22 +193,33 @@ class Order(models.Model): 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) + # Oprava: ManyToOneRel není pole. Potřebujeme iterovat přes self.discount.all(), proto ManyToManyField. + discount = models.ManyToManyField("DiscountCode", blank=True, related_name="orders") 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') + 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 + else: + total = Decimal('0.0') + # getting all prices from order items (without discount) - # getting all prices from order items (with discount applied if valid) for item in self.items.all(): - total = total + item.get_total_price(discount) + total = total + (item.product.price * item.quantity) return total + def save(self, *args, **kwargs): # Keep total_price always in sync with items and discount @@ -208,6 +247,10 @@ class OrderItem(models.Model): else: raise ValueError("Discount code must have either percent or amount defined.") + + elif not discount_object: + return self.quantity * self.product.price + else: return ValueError("Invalid discount code.") diff --git a/backend/thirdparty/zasilkovna/__init__.py b/backend/thirdparty/zasilkovna/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/zasilkovna/admin.py b/backend/thirdparty/zasilkovna/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/thirdparty/zasilkovna/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/thirdparty/zasilkovna/apps.py b/backend/thirdparty/zasilkovna/apps.py new file mode 100644 index 0000000..bb5d2f9 --- /dev/null +++ b/backend/thirdparty/zasilkovna/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ZasilkovnaConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'zasilkovna' diff --git a/backend/thirdparty/zasilkovna/migrations/__init__.py b/backend/thirdparty/zasilkovna/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/zasilkovna/models.py b/backend/thirdparty/zasilkovna/models.py new file mode 100644 index 0000000..4d8fd61 --- /dev/null +++ b/backend/thirdparty/zasilkovna/models.py @@ -0,0 +1,18 @@ +from django.db import models + +# Create your models here. + +class Zasilkovna(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + packet_id = models.CharField(max_length=255, unique=True) # identifikátor od Packety + label_pdf = models.BinaryField(null=True, blank=True) # uložit PDF binárně nebo odkaz + + status = models.CharField(max_length=50) # např. „created“, „in_transit“, „delivered“ + cancelled = models.BooleanField(default=False) + metadata = models.JSONField(default=dict, blank=True) # pro případná extra data + + def save(self, *args, **kwargs): + + # případná logika před uložením + super().save(*args, **kwargs) \ No newline at end of file diff --git a/backend/thirdparty/zasilkovna/tests.py b/backend/thirdparty/zasilkovna/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/thirdparty/zasilkovna/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/thirdparty/zasilkovna/views.py b/backend/thirdparty/zasilkovna/views.py new file mode 100644 index 0000000..27cdb63 --- /dev/null +++ b/backend/thirdparty/zasilkovna/views.py @@ -0,0 +1,4 @@ +from django.shortcuts import render + +# Create your views here. + diff --git a/frontend/index.html b/frontend/index.html index 422c3c3..31271be 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,19 +5,23 @@ - Vontor.cz – Creative Tech & Design Portfolio - - - - - - - - - - - - + + Vontor.cz – Creative Tech & Design Portfolio + + + + + + + + + + + + + + +