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.
317 lines
8.0 KiB
Python
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"]
|
|
|
|
|