Refactor commerce models and enhance payment logic
Refactored commerce models to remove language prefixes from status choices, improved order and payment validation, and enforced business rules for payment and shipping combinations. Updated order item and cart calculations to use VAT-inclusive prices, added unique constraints and indexes to reviews, and improved stock management logic. Added new Stripe client methods for session and refund management, and updated Zasilkovna and Deutsche Post models for consistency. Minor fixes and improvements across related tasks, URLs, and configuration models.
This commit is contained in:
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
20
backend/thirdparty/deutschepost/models.py
vendored
20
backend/thirdparty/deutschepost/models.py
vendored
@@ -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)
|
||||
|
||||
|
||||
126
backend/thirdparty/stripe/client.py
vendored
126
backend/thirdparty/stripe/client.py
vendored
@@ -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)})
|
||||
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"}
|
||||
19
backend/thirdparty/stripe/models.py
vendored
19
backend/thirdparty/stripe/models.py
vendored
@@ -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
|
||||
|
||||
5
backend/thirdparty/zasilkovna/client.py
vendored
5
backend/thirdparty/zasilkovna/client.py
vendored
@@ -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)
|
||||
|
||||
14
backend/thirdparty/zasilkovna/models.py
vendored
14
backend/thirdparty/zasilkovna/models.py
vendored
@@ -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 -------
|
||||
|
||||
Reference in New Issue
Block a user