diff --git a/backend/advertisement/tasks.py b/backend/advertisement/tasks.py index e9d97c5..d9f6874 100644 --- a/backend/advertisement/tasks.py +++ b/backend/advertisement/tasks.py @@ -2,6 +2,7 @@ from account.tasks import send_email_with_context from configuration.models import SiteConfiguration from celery import shared_task +from celery.schedules import crontab @shared_task def send_contact_me_email_task(client_email, message_content): @@ -14,4 +15,16 @@ def send_contact_me_email_task(client_email, message_content): subject="Poptávka z kontaktního formuláře!!!", template_path="email/contact_me.html", context=context, + ) + + +def send_newly_added_items_to_store_email_task_last_week(item_id): + + send_email_with_context( + recipients=SiteConfiguration.get_solo().contact_email, + subject="Nový produkt přidán do obchodu", + template_path="email/new_item_added.html", + context={ + "item": item, + } ) \ No newline at end of file diff --git a/backend/commerce/models.py b/backend/commerce/models.py index cb53fd2..8aad58b 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -1,3 +1,5 @@ +from ast import Or +import dis from django.db import models from django.conf import settings from django.utils import timezone @@ -5,7 +7,7 @@ from django.core.exceptions import ValidationError from decimal import Decimal from django.template.loader import render_to_string from django.core.files.base import ContentFile -from django.core.validators import MaxValueValidator, MinValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator, validate_email try: from weasyprint import HTML @@ -146,12 +148,12 @@ class ProductImage(models.Model): class Order(models.Model): class OrderStatus(models.TextChoices): - CREATED = "created", "cz#Vytvořeno" - CANCELLED = "cancelled", "cz#Zrušeno" - COMPLETED = "completed", "cz#Dokončeno" + CREATED = "created", "Vytvořeno" + CANCELLED = "cancelled", "Zrušeno" + COMPLETED = "completed", "Dokončeno" - REFUNDING = "refunding", "cz#Vrácení v procesu" - REFUNDED = "refunded", "cz#Vráceno" + REFUNDING = "refunding", "Vrácení v procesu" + REFUNDED = "refunded", "Vráceno" status = models.CharField( max_length=20, choices=OrderStatus.choices, null=True, blank=True, default=OrderStatus.CREATED @@ -216,10 +218,10 @@ class Order(models.Model): else: total = Decimal('0.0') - # getting all prices from order items (without discount) - + # getting all prices from order items (without discount) - using VAT-inclusive prices + for item in self.items.all(): - total = total + (item.product.price * item.quantity) + total = total + (item.product.get_price_with_vat() * item.quantity) return total + carrier_price @@ -234,6 +236,24 @@ class Order(models.Model): self.postal_code = self.user.postal_code self.country = self.user.country + def clean(self): + """Validate order data""" + # Validate required fields + required_fields = ['first_name', 'last_name', 'email', 'address', 'city', 'postal_code'] + for field in required_fields: + if not getattr(self, field): + raise ValidationError(f"{field.replace('_', ' ').title()} is required.") + + # Validate email format + try: + validate_email(self.email) + except ValidationError: + raise ValidationError("Invalid email format.") + + # Validate order has items + if self.pk and not self.items.exists(): + raise ValidationError("Order must have at least one item.") + def save(self, *args, **kwargs): # Keep total_price always in sync with items and discount self.total_price = self.calculate_total_price() @@ -255,16 +275,16 @@ class Order(models.Model): class Carrier(models.Model): class SHIPPING(models.TextChoices): - ZASILKOVNA = "packeta", "cz#Zásilkovna" - DEUTSCHEPOST = "deutschepost", "cz#Deutsche Post" - STORE = "store", "cz#Osobní odběr" + ZASILKOVNA = "packeta", "Zásilkovna" + DEUTSCHEPOST = "deutschepost", "Deutsche Post" + STORE = "store", "Osobní odběr" shipping_method = models.CharField(max_length=20, choices=SHIPPING.choices, default=SHIPPING.STORE) class STATE(models.TextChoices): - PREPARING = "ordered", "cz#Objednávka se připravuje" - SHIPPED = "shipped", "cz#Odesláno" - DELIVERED = "delivered", "cz#Doručeno" - READY_TO_PICKUP = "ready_to_pickup", "cz#Připraveno k vyzvednutí" + PREPARING = "ordered", "Objednávka se připravuje" + SHIPPED = "shipped", "Odesláno" + DELIVERED = "delivered", "Doručeno" + READY_TO_PICKUP = "ready_to_pickup", "Připraveno k vyzvednutí" #RETURNING = "returning", "Vracení objednávky" state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.PREPARING) @@ -285,27 +305,44 @@ class Carrier(models.Model): shipping_price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00')) def save(self, *args, **kwargs): - if self.pk is None: - - if self.shipping_price is None: - self.shipping_price = self.get_price() + # Set shipping price for new carriers + if self.pk is None and self.shipping_price is None: + # For new carriers, we might not have an order yet + self.shipping_price = self.get_price(order=None) + # Check if state changed to ready for pickup + old_state = None if self.pk: - if self.STATE == self.STATE.READY_TO_PICKUP and self.shipping_method == self.SHIPPING.STORE: - notify_Ready_to_pickup.delay(order=self.order, user=self.order.user) - pass - else: - pass + old_carrier = Carrier.objects.filter(pk=self.pk).first() + old_state = old_carrier.state if old_carrier else None super().save(*args, **kwargs) - def get_price(self): + # Send notification if state changed to ready for pickup + if (old_state != self.STATE.READY_TO_PICKUP and + self.state == self.STATE.READY_TO_PICKUP and + self.shipping_method == self.SHIPPING.STORE): + + if hasattr(self, 'order') and self.order: + notify_Ready_to_pickup.delay(order=self.order, user=self.order.user) + + def get_price(self, order=None): if self.shipping_method == self.SHIPPING.ZASILKOVNA: return SiteConfiguration.get_solo().zasilkovna_shipping_price elif self.shipping_method == self.SHIPPING.DEUTSCHEPOST: return SiteConfiguration.get_solo().deutschepost_shipping_price - else: + elif self.shipping_method == self.SHIPPING.STORE: + # Store pickup is always free return Decimal('0.0') + else: + # Check for free shipping based on order total + if order is None: + order = Order.objects.filter(carrier=self).first() + + if order and order.total_price >= SiteConfiguration.get_solo().free_shipping_over: + return Decimal('0.0') + else: + return SiteConfiguration.get_solo().default_shipping_price or Decimal('50.0') # fallback price #tohle bude vyvoláno pomocí admina přes api!!! @@ -363,9 +400,9 @@ class Carrier(models.Model): class Payment(models.Model): class PAYMENT(models.TextChoices): - SHOP = "shop", "cz#Platba v obchodě", "de#Bezahlung im Geschäft" - STRIPE = "stripe", "cz#Platební Brána", "de#Zahlungsgateway" - CASH_ON_DELIVERY = "cash_on_delivery", "cz#Dobírka", "de#Nachnahme" + SHOP = "shop", "Platba v obchodě" + STRIPE = "stripe", "Platební Brána" + CASH_ON_DELIVERY = "cash_on_delivery", "Dobírka" payment_method = models.CharField(max_length=30, choices=PAYMENT.choices, default=PAYMENT.SHOP) #FIXME: potvrdit že logika platby funguje správně @@ -373,16 +410,56 @@ class Payment(models.Model): stripe = models.OneToOneField( StripeModel, on_delete=models.CASCADE, null=True, blank=True, related_name="payment" ) + payed_at_shop = models.BooleanField(default=False, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def clean(self): """Validate payment and shipping method combinations""" - # TODO: Add validation logic for invalid payment/shipping combinations - # TODO: Skip GoPay integration for now + # Validate payment method consistency + if self.payment_method == self.PAYMENT.STRIPE and not self.stripe: + raise ValidationError("Stripe payment method requires a linked StripeModel instance.") + + elif self.payment_method == self.PAYMENT.SHOP and self.stripe: + raise ValidationError("Shop payment method should not have a linked StripeModel instance.") + + # Validate payment and shipping compatibility + if self.payment_method == self.PAYMENT.SHOP: + # SHOP payment only works with STORE pickup - customer pays at physical store + if Order.objects.filter(payment=self).exists(): + order = Order.objects.get(payment=self) + + if order.carrier and order.carrier.shipping_method != Carrier.SHIPPING.STORE: + raise ValidationError( + "Shop payment is only compatible with store pickup. " + "For shipping orders, use Stripe or Cash on Delivery payment methods." + ) + + elif self.payment_method == self.PAYMENT.CASH_ON_DELIVERY: + # Cash on delivery only works with shipping methods (not store pickup) + if Order.objects.filter(payment=self).exists(): + order = Order.objects.get(payment=self) + + if order.carrier and order.carrier.shipping_method == Carrier.SHIPPING.STORE: + raise ValidationError( + "Cash on delivery is not compatible with store pickup. " + "For store pickup, use shop payment method." + ) + + # STRIPE payment works with all shipping methods - no additional validation needed + super().clean() + def payed_manually(self): + """Mark payment as completed""" + if self.payment_method == self.PAYMENT.SHOP: + self.payed_at_shop = True + self.save() + else: + raise ValidationError("Manuální platba je povolena pouze pro platbu v obchodě.") + + # ------------------ SLEVOVÉ KÓDY ------------------ @@ -441,7 +518,7 @@ class OrderItem(models.Model): product = models.ForeignKey("commerce.Product", on_delete=models.PROTECT) quantity = models.PositiveIntegerField(default=1) - def get_total_price(self, discounts: list[DiscountCode] = None): + def get_total_price(self, discounts: list[DiscountCode] = list()): """Vrátí celkovou cenu položky po aplikaci relevantních kupónů. Logika dle SiteConfiguration: @@ -453,12 +530,12 @@ class OrderItem(models.Model): - Kombinace: nejprve procentuální část, poté odečtení fixní částky. - Sleva se nikdy nesmí dostat pod 0. """ - base_price = self.product.price * self.quantity + # Use VAT-inclusive price for customer-facing calculations + base_price = self.product.get_price_with_vat() * self.quantity - if not discounts: + if not discounts or discounts == []: return base_price - config = SiteConfiguration.get_solo() #seznám slev @@ -527,16 +604,19 @@ class OrderItem(models.Model): def save(self, *args, **kwargs): if self.pk is None: - if self.order.payment.payment_method: - raise ValueError("Nelze upravit položky z objednávky s již zvolenou platební metodou.") + # Check if order already has a processed payment + if (self.order.payment and + self.order.payment.payment_method and + self.order.payment.payment_method != Payment.PAYMENT.SHOP): + raise ValueError("Cannot modify items from order with processed payment method.") - else: - #nová položka objednávky, snížit skladové zásoby - if self.product.stock < self.quantity: - raise ValueError("Nedostatečný skladový zásob pro produkt.") + # Validate stock availability + if self.product.stock < self.quantity: + raise ValueError(f"Insufficient stock for product {self.product.name}. Available: {self.product.stock}") - self.product.stock -= self.quantity - self.product.save(update_fields=["stock"]) + # Reduce stock + self.product.stock -= self.quantity + self.product.save(update_fields=["stock"]) super().save(*args, **kwargs) @@ -545,10 +625,10 @@ class Refund(models.Model): order = models.ForeignKey(Order, related_name="refunds", on_delete=models.CASCADE) class Reason(models.TextChoices): - RETUNING_PERIOD = "retuning_before_fourteen_day_period", "cz#Vrácení před uplynutím 14-ti denní lhůty" - DAMAGED_PRODUCT = "damaged_product", "cz#Poškozený produkt" - WRONG_ITEM = "wrong_item", "cz#Špatná položka" - OTHER = "other", "cz#Jiný důvod" + RETUNING_PERIOD = "retuning_before_fourteen_day_period", "Vrácení před uplynutím 14-ti denní lhůty" + DAMAGED_PRODUCT = "damaged_product", "Poškozený produkt" + WRONG_ITEM = "wrong_item", "Špatná položka" + OTHER = "other", "Jiný důvod" reason_choice = models.CharField(max_length=40, choices=Reason.choices) reason_text = models.TextField(blank=True) @@ -635,17 +715,15 @@ class Refund(models.Model): "return_reason": return_reason, } - - carrier = models.OneToOneField( - "Carrier", - on_delete=models.CASCADE, - related_name="refund", - null=True, - blank=True - ) - - html_string = render_to_string("refund/customer_in_package_returning_form.html", context) + + # Import WeasyPrint lazily to avoid startup failures when system + # libraries (Pango/GObject) are not present on Windows. + if HTML is None: + raise RuntimeError( + "WeasyPrint is not available. Install its system dependencies (Pango/GTK) or run the backend in Docker." + ) + pdf_bytes = HTML(string=html_string).write_pdf() return pdf_bytes @@ -665,6 +743,7 @@ class Invoice(models.Model): order = Order.objects.get(invoice=self) # Render HTML html_string = render_to_string("invoice/Order.html", {"invoice": self, "order": order}) + # Import WeasyPrint lazily to avoid startup failures when system # libraries (Pango/GObject) are not present on Windows. if HTML is None: @@ -693,6 +772,19 @@ class Review(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta: + unique_together = ('product', 'user') # Prevent multiple reviews per user per product + indexes = [ + models.Index(fields=['product', 'rating']), + models.Index(fields=['created_at']), + ] + + def clean(self): + """Validate that user hasn't already reviewed this product""" + if self.pk is None: # Only for new reviews + if Review.objects.filter(product=self.product, user=self.user).exists(): + raise ValidationError("User has already reviewed this product.") + def __str__(self): return f"Review for {self.product.name} by {self.user.username}" @@ -727,7 +819,7 @@ class Cart(models.Model): return f"Anonymous cart ({self.session_key})" def get_total(self): - """Calculate total price of all items in cart""" + """Calculate total price of all items in cart including VAT""" total = Decimal('0.0') for item in self.items.all(): total += item.get_subtotal() @@ -754,8 +846,8 @@ class CartItem(models.Model): return f"{self.quantity}x {self.product.name} in cart" def get_subtotal(self): - """Calculate subtotal for this cart item""" - return self.product.price * self.quantity + """Calculate subtotal for this cart item including VAT""" + return self.product.get_price_with_vat() * self.quantity def clean(self): """Validate that product has enough stock""" diff --git a/backend/commerce/tasks.py b/backend/commerce/tasks.py index ac1d5e6..a7199bb 100644 --- a/backend/commerce/tasks.py +++ b/backend/commerce/tasks.py @@ -83,6 +83,24 @@ def notify_order_successfuly_created(order = None, user = None, **kwargs): pass +@shared_task +def notify_order_payed(order = None, user = None, **kwargs): + if not order or not user: + raise ValueError("Order and User must be provided for notification.") + + if kwargs: + print("Additional kwargs received in notify_order_paid:", kwargs) + + send_email_with_context( + recipients=user.email, + subject="Your order has been paid", + template_path="email/order_paid.html", + context={ + "user": user, + "order": order, + }) + + pass @shared_task def notify_about_missing_payment(order = None, user = None, **kwargs): @@ -103,6 +121,9 @@ def notify_about_missing_payment(order = None, user = None, **kwargs): pass + +# -- NOTIFICATIONS REFUND -- + @shared_task def notify_refund_items_arrived(order = None, user = None, **kwargs): if not order or not user: @@ -141,4 +162,7 @@ def notify_refund_accepted(order = None, user = None, **kwargs): "order": order, }) - pass \ No newline at end of file + pass + + +# \ No newline at end of file diff --git a/backend/commerce/urls.py b/backend/commerce/urls.py index 6a8c2da..4c8d90f 100644 --- a/backend/commerce/urls.py +++ b/backend/commerce/urls.py @@ -13,7 +13,7 @@ from .views import ( CartViewSet, WishlistViewSet, AdminWishlistViewSet, - AnalyticsViewSet, + AnalyticsView, ) router = DefaultRouter() @@ -27,10 +27,10 @@ router.register(r'reviews', ReviewPublicViewSet, basename='review') router.register(r'cart', CartViewSet, basename='cart') router.register(r'wishlist', WishlistViewSet, basename='wishlist') router.register(r'admin/wishlists', AdminWishlistViewSet, basename='admin-wishlist') -router.register(r'analytics', AnalyticsViewSet, basename='analytics') urlpatterns = [ path('', include(router.urls)), path('refunds/public/', RefundPublicView.as_view(), name='RefundPublicView'), path('reviews/create/', ReviewPostPublicView.as_view(), name='ReviewCreate'), + path('analytics/', AnalyticsView.as_view(), name='analytics'), ] diff --git a/backend/configuration/models.py b/backend/configuration/models.py index 6857af6..6abf4bf 100644 --- a/backend/configuration/models.py +++ b/backend/configuration/models.py @@ -44,8 +44,8 @@ class SiteConfiguration(models.Model): 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" + CZK = "CZK", "Czech Koruna" + EUR = "EUR", "Euro" currency = models.CharField(max_length=10, default=CURRENCY.CZK, choices=CURRENCY.choices) class Meta: @@ -87,6 +87,10 @@ class VATRate(models.Model): 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: diff --git a/backend/thirdparty/deutschepost/models.py b/backend/thirdparty/deutschepost/models.py index 90398be..63a4a63 100644 --- a/backend/thirdparty/deutschepost/models.py +++ b/backend/thirdparty/deutschepost/models.py @@ -42,12 +42,12 @@ class DeutschePostOrder(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATE(models.TextChoices): - CREATED = "CREATED", "cz#Vytvořeno" - FINALIZED = "FINALIZED", "cz#Dokončeno" - SHIPPED = "SHIPPED", "cz#Odesláno" - DELIVERED = "DELIVERED", "cz#Doručeno" - CANCELLED = "CANCELLED", "cz#Zrušeno" - ERROR = "ERROR", "cz#Chyba" + CREATED = "CREATED", "Vytvořeno" + FINALIZED = "FINALIZED", "Dokončeno" + SHIPPED = "SHIPPED", "Odesláno" + DELIVERED = "DELIVERED", "Doručeno" + CANCELLED = "CANCELLED", "Zrušeno" + ERROR = "ERROR", "Chyba" state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.CREATED) @@ -280,10 +280,10 @@ class DeutschePostBulkOrder(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATUS(models.TextChoices): - CREATED = "CREATED", "cz#Vytvořeno" - PROCESSING = "PROCESSING", "cz#Zpracovává se" - COMPLETED = "COMPLETED", "cz#Dokončeno" - ERROR = "ERROR", "cz#Chyba" + CREATED = "CREATED", "Vytvořeno" + PROCESSING = "PROCESSING", "Zpracovává se" + COMPLETED = "COMPLETED", "Dokončeno" + ERROR = "ERROR", "Chyba" status = models.CharField(max_length=20, choices=STATUS.choices, default=STATUS.CREATED) diff --git a/backend/thirdparty/stripe/client.py b/backend/thirdparty/stripe/client.py index d0c7e6e..ce5ad4e 100644 --- a/backend/thirdparty/stripe/client.py +++ b/backend/thirdparty/stripe/client.py @@ -10,6 +10,7 @@ stripe.api_key = os.getenv("STRIPE_SECRET_KEY") class StripeClient: + @staticmethod def create_checkout_session(order): """ Vytvoří Stripe Checkout Session pro danou objednávku. @@ -42,8 +43,64 @@ class StripeClient: return session + @staticmethod + def cancel_checkout_session(session_id): + """ + Zruší Stripe Checkout Session. + Args: + session_id (str): ID Stripe Checkout Session k zrušení. + + Returns: + stripe.checkout.Session: Zrušená Stripe Checkout Session. + """ + try: + session = stripe.checkout.Session.expire(session_id) + return session + except Exception as e: + return {"error": str(e)} + @staticmethod + def get_checkout_session(session_id): + """ + Získá informace o Stripe Checkout Session. + Args: + session_id (str): ID Stripe Checkout Session. + + Returns: + stripe.checkout.Session: Stripe Checkout Session objekt. + """ + try: + session = stripe.checkout.Session.retrieve(session_id) + return session + except Exception as e: + return {"error": str(e)} + + @staticmethod + def get_payment_intent(payment_intent_id): + """ + Získá informace o Stripe Payment Intent. + Args: + payment_intent_id (str): ID Stripe Payment Intent. + + Returns: + stripe.PaymentIntent: Stripe Payment Intent objekt. + """ + try: + payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) + return payment_intent + except Exception as e: + return {"error": str(e)} + + @staticmethod def refund_order(stripe_payment_intent): + """ + Vrátí platbu pro danou objednávku. + Args: + stripe_payment_intent (str): ID Stripe Payment Intent k vrácení. + + Returns: + stripe.Refund: Vytvořený refund objekt nebo chyba. + """ try: refund = stripe.Refund.create( payment_intent=stripe_payment_intent @@ -51,4 +108,71 @@ class StripeClient: return refund except Exception as e: - return json.dumps({"error": str(e)}) \ No newline at end of file + return {"error": str(e)} + + @staticmethod + def partial_refund_order(stripe_payment_intent, amount): + """ + Částečně vrátí platbu pro danou objednávku. + Args: + stripe_payment_intent (str): ID Stripe Payment Intent k vrácení. + amount (int): Částka k vrácení v haléřích. + + Returns: + stripe.Refund: Vytvořený refund objekt nebo chyba. + """ + try: + refund = stripe.Refund.create( + payment_intent=stripe_payment_intent, + amount=amount + ) + return refund + + except Exception as e: + return {"error": str(e)} + + @staticmethod + def create_customer(email, name=None, phone=None): + """ + Vytvoří Stripe Customer. + Args: + email (str): Email zákazníka. + name (str, optional): Jméno zákazníka. + phone (str, optional): Telefon zákazníka. + + Returns: + stripe.Customer: Vytvořený Stripe Customer. + """ + try: + customer_data = {"email": email} + if name: + customer_data["name"] = name + if phone: + customer_data["phone"] = phone + + customer = stripe.Customer.create(**customer_data) + return customer + except Exception as e: + return {"error": str(e)} + + @staticmethod + def webhook_verify_signature(payload, sig_header, endpoint_secret): + """ + Ověří webhook signature od Stripe. + Args: + payload: Raw webhook payload. + sig_header: Stripe signature header. + endpoint_secret: Webhook endpoint secret. + + Returns: + stripe.Event: Ověřený event objekt. + """ + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + return event + except ValueError as e: + return {"error": "Invalid payload"} + except stripe.error.SignatureVerificationError as e: + return {"error": "Invalid signature"} \ No newline at end of file diff --git a/backend/thirdparty/stripe/models.py b/backend/thirdparty/stripe/models.py index cf82a90..49b8b05 100644 --- a/backend/thirdparty/stripe/models.py +++ b/backend/thirdparty/stripe/models.py @@ -1,5 +1,6 @@ from django.db import models from django.apps import apps +from django.utils import timezone # Create your models here. @@ -29,23 +30,25 @@ class StripeModel(models.Model): def save(self, *args, **kwargs): - #if new - if self.pk: + # If new (no primary key yet) + if self.pk is None: + super().save(*args, **kwargs) # Save first to get pk + Order = apps.get_model('commerce', 'Order') Payment = apps.get_model('commerce', 'Payment') order = Order.objects.get(payment=Payment.objects.get(stripe=self)) - - session = StripeClient.create_checkout_session(order)# <-- předáme self.StripePayment + session = StripeClient.create_checkout_session(order) self.stripe_session_id = session.id self.stripe_payment_intent = session.payment_intent self.stripe_session_url = session.url - + + # Save again with Stripe data + super().save(update_fields=['stripe_session_id', 'stripe_payment_intent', 'stripe_session_url', 'updated_at']) else: - self.updated_at = models.DateTimeField(auto_now=True) - - super().save(*args, **kwargs) + self.updated_at = timezone.now() + super().save(*args, **kwargs) def paid(self): self.status = self.STATUS_CHOICES.PAID diff --git a/backend/thirdparty/zasilkovna/client.py b/backend/thirdparty/zasilkovna/client.py index 76a3005..5d0ebb9 100644 --- a/backend/thirdparty/zasilkovna/client.py +++ b/backend/thirdparty/zasilkovna/client.py @@ -3,7 +3,7 @@ from zeep.exceptions import Fault from zeep.transports import Transport from zeep.cache import SqliteCache -from configuration.models import Configuration +from configuration.models import SiteConfiguration import tempfile import base64 @@ -17,9 +17,6 @@ logger = logging.getLogger(__name__) WSDL_URL = os.getenv("PACKETA_WSDL_URL", "https://www.zasilkovna.cz/api/soap.wsdl") PACKETA_API_PASSWORD = os.getenv("PACKETA_API_PASSWORD") -Configuration.g - - # --- 1. Singleton pro klienta (aby se WSDL stáhlo jen jednou pro celý proces) --- @lru_cache(maxsize=1) diff --git a/backend/thirdparty/zasilkovna/models.py b/backend/thirdparty/zasilkovna/models.py index ff568d3..75bfdd4 100644 --- a/backend/thirdparty/zasilkovna/models.py +++ b/backend/thirdparty/zasilkovna/models.py @@ -35,14 +35,14 @@ class ZasilkovnaPacket(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATE(models.TextChoices): - WAITING_FOR_ORDER = "WAITING_FOR_ORDERING_SHIPMENT", "cz#Čeká na objednání zásilkovny" - PENDING = "PENDING", "cz#Podáno" - SENDED = "SENDED", "cz#Odesláno" - ARRIVED = "ARRIVED", "cz#Doručeno" - CANCELED = "CANCELED", "cz#Zrušeno" + WAITING_FOR_ORDER = "WAITING_FOR_ORDERING_SHIPMENT", "Čeká na objednání zásilkovny" + PENDING = "PENDING", "Podáno" + SENDED = "SENDED", "Odesláno" + ARRIVED = "ARRIVED", "Doručeno" + CANCELED = "CANCELED", "Zrušeno" - RETURNING = "RETURNING", "cz#Posláno zpátky" - RETURNED = "RETURNED", "cz#Vráceno" + RETURNING = "RETURNING", "Posláno zpátky" + RETURNED = "RETURNED", "Vráceno" state = models.CharField(max_length=35, choices=STATE.choices, default=STATE.PENDING) # ------- API -------