Files
vontor-cz/backend/commerce/serializers.py
Brunobrno e86839f2da Add public refund creation endpoint and PDF generation
Introduces RefundPublicView for public refund creation via email and invoice/order ID, returning refund info and a base64-encoded PDF slip. Adds RefundCreatePublicSerializer for validation and creation, implements PDF generation in Refund model, and provides a customer-facing HTML template for the return slip. Updates URLs to expose the new endpoint.
2025-11-19 00:53:37 +01:00

317 lines
8.0 KiB
Python

from rest_framework import serializers
from .models import Refund, Order, Invoice
class RefundCreatePublicSerializer(serializers.Serializer):
email = serializers.EmailField()
invoice_number = serializers.CharField(required=False, allow_blank=True)
order_id = serializers.IntegerField(required=False)
# Optional reason fields
reason_choice = serializers.ChoiceField(
choices=Refund.Reason.choices, required=False
)
reason_text = serializers.CharField(required=False, allow_blank=True)
def validate(self, attrs):
email = attrs.get("email")
invoice_number = (attrs.get("invoice_number") or "").strip()
order_id = attrs.get("order_id")
if not invoice_number and not order_id:
raise serializers.ValidationError(
"Provide either invoice_number or order_id."
)
order = None
if invoice_number:
try:
invoice = Invoice.objects.get(invoice_number=invoice_number)
order = invoice.order
except Invoice.DoesNotExist:
raise serializers.ValidationError({"invoice_number": "Invoice not found."})
except Order.DoesNotExist:
raise serializers.ValidationError({"invoice_number": "Order for invoice not found."})
if order_id and order is None:
try:
order = Order.objects.get(id=order_id)
except Order.DoesNotExist:
raise serializers.ValidationError({"order_id": "Order not found."})
# Verify email matches order's email or user's email
if not order:
raise serializers.ValidationError("Order could not be resolved.")
order_email = (order.email or "").strip().lower()
user_email = (getattr(order.user, "email", "") or "").strip().lower()
provided = email.strip().lower()
if provided not in {order_email, user_email}:
raise serializers.ValidationError({"email": "Email does not match the order."})
attrs["order"] = order
return attrs
def create(self, validated_data):
order = validated_data["order"]
reason_choice = validated_data.get("reason_choice") or Refund.Reason.OTHER
reason_text = validated_data.get("reason_text", "")
refund = Refund.objects.create(
order=order,
reason_choice=reason_choice,
reason_text=reason_text,
)
return refund
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from decimal import Decimal
from django.contrib.auth import get_user_model
from .models import (
Category,
Product,
ProductImage,
DiscountCode,
Refund,
Order,
OrderItem,
Carrier,
Payment,
)
User = get_user_model()
class UserBriefSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "first_name", "last_name", "email"]
class ProductBriefSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id", "name", "price"]
class OrderItemReadSerializer(serializers.ModelSerializer):
product = ProductBriefSerializer(read_only=True)
total_price = serializers.SerializerMethodField()
def get_total_price(self, obj):
return obj.get_total_price(list(obj.order.discount.all()) if getattr(obj, "order", None) else None)
class Meta:
model = OrderItem
fields = ["id", "product", "quantity", "total_price"]
read_only_fields = ["id", "total_price"]
class CarrierReadSerializer(serializers.ModelSerializer):
class Meta:
model = Carrier
fields = ["id", "shipping_method", "weight", "returning"]
read_only_fields = ["id"]
class PaymentReadSerializer(serializers.Serializer):
payment_method = serializers.CharField(read_only=True)
stripe_id = serializers.IntegerField(source="stripe.id", read_only=True, allow_null=True)
class OrderReadSerializer(serializers.ModelSerializer):
items = OrderItemReadSerializer(many=True, read_only=True)
carrier = CarrierReadSerializer(read_only=True)
payment = PaymentReadSerializer(source="payment", read_only=True)
user = serializers.SerializerMethodField()
def get_user(self, obj):
request = self.context.get("request") if hasattr(self, "context") else None
if request and getattr(request, "user", None) and request.user.is_authenticated and obj.user:
return UserBriefSerializer(obj.user).data
return None
class Meta:
model = Order
fields = [
"id",
"status",
"total_price",
"currency",
"first_name",
"last_name",
"email",
"phone",
"address",
"city",
"postal_code",
"country",
"note",
"created_at",
"updated_at",
"carrier",
"payment",
"user",
"items",
]
read_only_fields = [
"id",
"status",
"total_price",
"currency",
"created_at",
"updated_at",
]
class OrderMiniSerializer(serializers.ModelSerializer):
amount = serializers.DecimalField(max_digits=10, decimal_places=2, source="total_price", read_only=True)
shipping_method = serializers.CharField(source="carrier.shipping_method", read_only=True)
class Meta:
model = Order
fields = ["id", "amount", "status", "email", "shipping_method"]
read_only_fields = fields
# ----------------- CREATE PAYLOAD SERIALIZERS (PUBLIC) -----------------
class OrderItemCreateSerializer(serializers.Serializer):
product_id = serializers.IntegerField(label="Product ID")
quantity = serializers.IntegerField(min_value=1, label="Quantity")
class CarrierCreateSerializer(serializers.Serializer):
shipping_method = serializers.ChoiceField(
choices=Carrier.SHIPPING.choices,
label="Shipping Method",
help_text="Choose 'store' (pickup) or 'packeta'",
)
weight = serializers.DecimalField(
required=False,
max_digits=10,
decimal_places=2,
label="Weight (kg)",
)
class PaymentCreateSerializer(serializers.Serializer):
payment_method = serializers.ChoiceField(
choices=Payment.PAYMENT.choices,
label="Payment Method",
help_text="Choose 'shop', 'stripe' or 'cash_on_delivery'",
)
class OrderCreateSerializer(serializers.Serializer):
# Customer/billing
first_name = serializers.CharField(max_length=100, label="First Name")
last_name = serializers.CharField(max_length=100, label="Last Name")
email = serializers.EmailField(label="Email")
phone = serializers.CharField(max_length=20, required=False, allow_blank=True, label="Phone")
address = serializers.CharField(max_length=255, label="Address")
city = serializers.CharField(max_length=100, label="City")
postal_code = serializers.CharField(max_length=20, label="Postal Code")
country = serializers.CharField(max_length=100, required=False, default="Czech Republic", label="Country")
note = serializers.CharField(required=False, allow_blank=True, label="Note")
# Nested
items = OrderItemCreateSerializer(many=True)
carrier = CarrierCreateSerializer()
payment = PaymentCreateSerializer()
discount_codes = serializers.ListField(
child=serializers.CharField(), required=False, allow_empty=True, label="Discount Codes"
)
def validate(self, attrs):
if not attrs.get("items"):
raise serializers.ValidationError({"items": "At least one item is required."})
return attrs
# ----------------- ADMIN/READ MODELS -----------------
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",
"product",
"image",
"alt_text",
"is_main",
]
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = [
"id",
"name",
"description",
"code",
"category",
"price",
"url",
"stock",
"is_active",
"limited_to",
"default_carrier",
"created_at",
"updated_at",
]
read_only_fields = ["created_at", "updated_at"]
class DiscountCodeSerializer(serializers.ModelSerializer):
class Meta:
model = DiscountCode
fields = [
"id",
"code",
"description",
"percent",
"amount",
"valid_from",
"valid_to",
"active",
"usage_limit",
"used_count",
"specific_products",
"specific_categories",
]
read_only_fields = ["used_count"]
class RefundSerializer(serializers.ModelSerializer):
class Meta:
model = Refund
fields = [
"id",
"order",
"reason_choice",
"reason_text",
"verified",
"created_at",
]
read_only_fields = ["id", "verified", "created_at"]