diff --git a/backend/commerce/models.py b/backend/commerce/models.py index 8a189bd..0ad855c 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -1,11 +1,14 @@ from django.db import models from django.conf import settings from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +#from django.utils.translation import gettext_lazy as _ from decimal import Decimal -from thirdparty.zasilkovna.models import PacketaShipment +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) @@ -77,31 +80,175 @@ class ProductImage(models.Model): return f"{self.product.name} image" -# ------------------ OBJENDÁVKOVÉ MODELY (dole) ------------------ +# ------------------ OBJEDNÁVKY ------------------ -# Dopravci a způsoby dopravy -class Carrier(models.Model): +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") - class Role(models.TextChoices): - ZASILKOVNA = "packeta", "Zásilkovna" - STORE = "store", "Osobní odběr" - - choice = models.CharField(max_length=20, choices=Role.choices, default=Role.STORE) - - # prodejce to přidá později - zasilkovna = models.ForeignKey( - 'thirdparty.zasilkovna.Zasilkovna', on_delete=models.DO_NOTHING, null=True, blank=True, related_name="carriers" + status = models.CharField( + max_length=20, choices=Status.choices, default=Status.PENDING ) - def __str__(self): - return f"{self.name} ({self.base_price} Kč)" + # 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) @@ -139,102 +286,17 @@ class DiscountCode(models.Model): def __str__(self): 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") - - 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 - 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) - 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) - - # 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): - 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) - - for item in self.items.all(): - total = total + (item.product.price * item.quantity) - - return total - - 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() - - super().save(*args, **kwargs) - +# ------------------ 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): + 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(): @@ -253,20 +315,7 @@ class OrderItem(models.Model): return self.quantity * self.product.price else: - return ValueError("Invalid discount code.") + raise ValueError("Invalid discount code.") def __str__(self): - return f"{self.product.name} x{self.quantity}" - - - - - - class Returning_order(models.Model): - #FIXME: dodělat !!! - order = models.ForeignKey(Order, related_name="returning_orders", on_delete=models.CASCADE) - reason = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"Returning Order #{self.order.id} - {self.created_at.strftime('%Y-%m-%d')}" \ No newline at end of file + return f"{self.product.name} x{self.quantity}" \ No newline at end of file diff --git a/backend/commerce/serializers.py b/backend/commerce/serializers.py index 3fbde37..ad5ba6b 100644 --- a/backend/commerce/serializers.py +++ b/backend/commerce/serializers.py @@ -2,89 +2,3 @@ from rest_framework import serializers from drf_spectacular.utils import extend_schema_field from .models import Category, Product, ProductImage, DiscountCode, Order, OrderItem, Carrier -# NOTE: Carrier intentionally skipped per request (TODO below) - -class CategorySerializer(serializers.ModelSerializer): - class Meta: - model = Category - fields = ["id", "name", "url", "parent", "description", "image"] - - -class ProductImageSerializer(serializers.ModelSerializer): - class Meta: - model = ProductImage - fields = ["id", "image", "alt_text", "is_main"] - - -class ProductSerializer(serializers.ModelSerializer): - images = ProductImageSerializer(many=True, read_only=True) - available = serializers.BooleanField(read_only=True) - - class Meta: - model = Product - fields = [ - "id","name","description","code","category","price","currency","url","stock","is_active","limited_to","default_carrier","available","images","created_at","updated_at" - ] - read_only_fields = ["created_at","updated_at","available"] - - -class DiscountCodeSerializer(serializers.ModelSerializer): - is_valid = serializers.SerializerMethodField() - - class Meta: - model = DiscountCode - fields = ["id","code","description","percent","amount","valid_from","valid_to","active","usage_limit","used_count","specific_products","specific_categories","is_valid"] - read_only_fields = ["used_count","is_valid"] - - def get_is_valid(self, obj): - return obj.is_valid() - - -class OrderItemSerializer(serializers.ModelSerializer): - product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all()) - line_total = serializers.SerializerMethodField() - - class Meta: - model = OrderItem - fields = ["id","product","quantity","line_total"] - read_only_fields = ["line_total"] - - def get_line_total(self, obj): - # Uses existing model logic for discount via order context (kept minimal) - # Since discount resolution logic is custom & currently incomplete, just returns base price * qty - return obj.product.price * obj.quantity - - -class OrderSerializer(serializers.ModelSerializer): - items = OrderItemSerializer(many=True) - total_price = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True) - - class Meta: - model = Order - fields = [ - "id","user","status","total_price","currency","first_name","last_name","email","phone","address","city","postal_code","country","note","discount","items","created_at","updated_at" - ] - read_only_fields = ["total_price","created_at","updated_at"] - - def create(self, validated_data): - items_data = validated_data.pop("items", []) - order = Order.objects.create(**validated_data) - for item in items_data: - OrderItem.objects.create(order=order, **item) - order.total_price = order.calculate_total_price() if hasattr(order, "calculate_total_price") else order.total_price - order.save() - return order - - def update(self, instance, validated_data): - items_data = validated_data.pop("items", None) - for attr, value in validated_data.items(): - setattr(instance, attr, value) - if items_data is not None: - instance.items.all().delete() - for item in items_data: - OrderItem.objects.create(order=instance, **item) - instance.total_price = instance.calculate_total_price() if hasattr(instance, "calculate_total_price") else instance.total_price - instance.save() - return instance - -# TODO: CarrierSerializer (Carrier API not requested yet) \ No newline at end of file diff --git a/backend/commerce/views.py b/backend/commerce/views.py index 1b21431..e582197 100644 --- a/backend/commerce/views.py +++ b/backend/commerce/views.py @@ -2,70 +2,3 @@ from rest_framework import viewsets from rest_framework.permissions import AllowAny from drf_spectacular.utils import extend_schema, extend_schema_view -from .models import Category, Product, ProductImage, DiscountCode, Order, OrderItem, Carrier -from .serializers import ( - CategorySerializer, - ProductSerializer, - ProductImageSerializer, - DiscountCodeSerializer, - OrderSerializer, -) - - -@extend_schema_view( - list=extend_schema(tags=["Commerce", "Categories"], summary="List categories"), - retrieve=extend_schema(tags=["Commerce", "Categories"], summary="Retrieve category"), - create=extend_schema(tags=["Commerce", "Categories"], summary="Create category"), - update=extend_schema(tags=["Commerce", "Categories"], summary="Update category"), - partial_update=extend_schema(tags=["Commerce", "Categories"], summary="Partial update category"), - destroy=extend_schema(tags=["Commerce", "Categories"], summary="Delete category"), -) -class CategoryViewSet(viewsets.ModelViewSet): - queryset = Category.objects.all() - serializer_class = CategorySerializer - permission_classes = [AllowAny] - - -@extend_schema_view( - list=extend_schema(tags=["Commerce", "Products"], summary="List products"), - retrieve=extend_schema(tags=["Commerce", "Products"], summary="Retrieve product"), - create=extend_schema(tags=["Commerce", "Products"], summary="Create product"), - update=extend_schema(tags=["Commerce", "Products"], summary="Update product"), - partial_update=extend_schema(tags=["Commerce", "Products"], summary="Partial update product"), - destroy=extend_schema(tags=["Commerce", "Products"], summary="Delete product"), -) -class ProductViewSet(viewsets.ModelViewSet): - queryset = Product.objects.all() - serializer_class = ProductSerializer - permission_classes = [AllowAny] - - -@extend_schema_view( - list=extend_schema(tags=["Commerce", "Discounts"], summary="List discount codes"), - retrieve=extend_schema(tags=["Commerce", "Discounts"], summary="Retrieve discount code"), - create=extend_schema(tags=["Commerce", "Discounts"], summary="Create discount code"), - update=extend_schema(tags=["Commerce", "Discounts"], summary="Update discount code"), - partial_update=extend_schema(tags=["Commerce", "Discounts"], summary="Partial update discount code"), - destroy=extend_schema(tags=["Commerce", "Discounts"], summary="Delete discount code"), -) -class DiscountCodeViewSet(viewsets.ModelViewSet): - queryset = DiscountCode.objects.all() - serializer_class = DiscountCodeSerializer - permission_classes = [AllowAny] - - -@extend_schema_view( - list=extend_schema(tags=["Commerce", "Orders"], summary="List orders"), - retrieve=extend_schema(tags=["Commerce", "Orders"], summary="Retrieve order"), - create=extend_schema(tags=["Commerce", "Orders"], summary="Create order"), - update=extend_schema(tags=["Commerce", "Orders"], summary="Update order"), - partial_update=extend_schema(tags=["Commerce", "Orders"], summary="Partial update order"), - destroy=extend_schema(tags=["Commerce", "Orders"], summary="Delete order"), -) -class OrderViewSet(viewsets.ModelViewSet): - queryset = Order.objects.all() - serializer_class = OrderSerializer - permission_classes = [AllowAny] - - -# TODO: CarrierViewSet & CarrierSerializer when requested diff --git a/backend/configuration/__init__.py b/backend/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/configuration/admin.py b/backend/configuration/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/configuration/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/configuration/apps.py b/backend/configuration/apps.py new file mode 100644 index 0000000..eab8aa0 --- /dev/null +++ b/backend/configuration/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ConfigurationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'configuration' diff --git a/backend/configuration/migrations/__init__.py b/backend/configuration/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/configuration/models.py b/backend/configuration/models.py new file mode 100644 index 0000000..c04e12b --- /dev/null +++ b/backend/configuration/models.py @@ -0,0 +1,29 @@ +from django.db import models + +# Create your models here. + +class ShopConfiguration(models.Model): + name = models.CharField(max_length=100, default="Shop name", unique=True) + + zasilkovna_shipping_price = models.DecimalField(max_digits=10, decimal_places=2, default=50) + zasilkovna_address_id = models.CharField(max_length=100, blank=True, null=True, help_text="ID výdejního místa Zásilkovny pro odesílání zásilek") + free_shipping_over = models.DecimalField(max_digits=10, decimal_places=2, default=2000) + + class CURRENCY(models.TextChoices): + CZK = "CZK", "Czech Koruna" + EUR = "EUR", "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 \ No newline at end of file diff --git a/backend/configuration/tests.py b/backend/configuration/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/configuration/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/configuration/views.py b/backend/configuration/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/configuration/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/thirdparty/stripe/models.py b/backend/thirdparty/stripe/models.py index 7f31142..4207bc1 100644 --- a/backend/thirdparty/stripe/models.py +++ b/backend/thirdparty/stripe/models.py @@ -1,8 +1,10 @@ from django.db import models # Create your models here. + +#TODO: logika a interakce bude na stripu (třeba aktualizovaní objednávky na zaplacenou apod.) -class Order(models.Model): +class StripePayment(models.Model): STATUS_CHOICES = [ ("pending", "Pending"), ("paid", "Paid"), diff --git a/backend/thirdparty/zasilkovna/apps.py b/backend/thirdparty/zasilkovna/apps.py index bb5d2f9..1d0558a 100644 --- a/backend/thirdparty/zasilkovna/apps.py +++ b/backend/thirdparty/zasilkovna/apps.py @@ -4,3 +4,5 @@ from django.apps import AppConfig class ZasilkovnaConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'zasilkovna' + name = 'thirdparty.zasilkovna' + label = "zasilkovna" diff --git a/backend/thirdparty/zasilkovna/client.py b/backend/thirdparty/zasilkovna/client.py index 98f8305..196d8d6 100644 --- a/backend/thirdparty/zasilkovna/client.py +++ b/backend/thirdparty/zasilkovna/client.py @@ -7,7 +7,7 @@ import os logger = logging.getLogger(__name__) -WSDL_URL = os.getenv("PACKETA_WSDL_URL", "https://soap.api.packeta.com/api/soap-bugfix.wsdl") +WSDL_URL = os.getenv("PACKETA_WSDL_URL", "https://www.zasilkovna.cz/api/soap.wsdl") PACKETA_API_PASSWORD = os.getenv("PACKETA_API_PASSWORD") zeepZasClient = Client(wsdl=WSDL_URL) diff --git a/backend/thirdparty/zasilkovna/models.py b/backend/thirdparty/zasilkovna/models.py index 4b381bc..831386a 100644 --- a/backend/thirdparty/zasilkovna/models.py +++ b/backend/thirdparty/zasilkovna/models.py @@ -23,6 +23,8 @@ from django.core.validators import RegexValidator from django.core.files.base import ContentFile from .client import PacketaAPI +from commerce.models import Order, Carrier +from configuration.models import Configuration packeta_client = PacketaAPI() # single reusable instance @@ -71,8 +73,27 @@ class ZasilkovnaPacket(models.Model): def save(self, *args, **kwargs): # On first save, create the packet remotely if packet_id is not set + carrier = Carrier.objects.get(zasilkovna=self) + order = Order.objects.get(carrier=carrier) + if not self.packet_id: - response = packeta_client.create_packet(**kwargs) + response = packeta_client.create_packet( + address_id=self.addressId, + weight=self.weight, + number=order.id, + name=order.first_name, + surname=order.last_name, + company=order.company, + email=order.email, + addressId=Configuration.get_solo().zasilkovna_address_id, + + #FIXME: udělat logiku pro počítaní dobírky a hodnoty zboží + cod=100.00, + value=100.00, + + currency=Configuration.get_solo().currency, + eshop= Configuration.get_solo().name, + ) self.packet_id = response['packet_id'] self.barcode = response['barcode'] diff --git a/backend/thirdparty/zasilkovna/views.py b/backend/thirdparty/zasilkovna/views.py index e69de29..04de252 100644 --- a/backend/thirdparty/zasilkovna/views.py +++ b/backend/thirdparty/zasilkovna/views.py @@ -0,0 +1,8 @@ +#views.py + +""" +TODO: OBJEDNAVANÍ SE VYVOLÁVA V CARRIER V COMMERCE.MODELS.PY + získaní labelu, + info o kurýrovi, vracení balíku, + vytvoření hromadné expedice +""" \ No newline at end of file diff --git a/backend/vontor_cz/settings.py b/backend/vontor_cz/settings.py index f24dda1..1c06467 100644 --- a/backend/vontor_cz/settings.py +++ b/backend/vontor_cz/settings.py @@ -332,6 +332,7 @@ MY_CREATED_APPS = [ 'thirdparty.downloader', 'thirdparty.stripe', # register Stripe app so its models are recognized 'thirdparty.trading212', + 'thirdparty.zasilkovna', 'thirdparty.gopay', # add GoPay app ]