forked from CGM_Public/pretix_original
..
This commit is contained in:
@@ -10,13 +10,20 @@ from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.base.models import Item, ItemVariation, SubEvent
|
||||
from pretix.base.models.orders import CartPosition, CheckoutSession
|
||||
from pretix.base.services.cart import add_items_to_cart, error_messages
|
||||
from pretix.base.models import Item, ItemVariation, SubEvent, TaxRule
|
||||
from pretix.base.models.orders import CartPosition, CheckoutSession, OrderFee
|
||||
from pretix.base.services.cart import (
|
||||
add_items_to_cart, error_messages, get_fees, set_cart_addons,
|
||||
)
|
||||
from pretix.base.storelogic.addons import get_addon_groups
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.presale.views.cart import generate_cart_id
|
||||
from pretix.storefrontapi.endpoints.event import (
|
||||
CategorySerializer, ItemSerializer,
|
||||
)
|
||||
from pretix.storefrontapi.permission import StorefrontEventPermission
|
||||
from pretix.storefrontapi.serializers import I18nFlattenedModelSerializer
|
||||
from pretix.storefrontapi.steps import get_steps
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,6 +40,24 @@ class CartAddLineSerializer(serializers.Serializer):
|
||||
voucher = serializers.CharField(allow_null=True, required=False)
|
||||
|
||||
|
||||
class CartAddonLineSerializer(CartAddLineSerializer):
|
||||
voucher = None
|
||||
addon_to = serializers.PrimaryKeyRelatedField(
|
||||
queryset=CartPosition.objects.none(), required=True
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["addon_to"].queryset = CartPosition.objects.filter(
|
||||
cart_id=self.context["cart_id"], addon_to__isnull=True
|
||||
)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
i = super().to_internal_value(data)
|
||||
i["addon_to"] = i["addon_to"].pk
|
||||
return i
|
||||
|
||||
|
||||
class InlineItemSerializer(I18nFlattenedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -64,6 +89,20 @@ class InlineSubEventSerializer(I18nFlattenedModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class CartFeeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = [
|
||||
"fee_type",
|
||||
"description",
|
||||
"value",
|
||||
"tax_rate",
|
||||
"tax_value",
|
||||
"internal_type",
|
||||
]
|
||||
|
||||
|
||||
class CartPositionSerializer(serializers.ModelSerializer):
|
||||
# todo: prefetch related items
|
||||
item = InlineItemSerializer(read_only=True)
|
||||
@@ -73,6 +112,8 @@ class CartPositionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = [
|
||||
"id",
|
||||
"addon_to",
|
||||
"item",
|
||||
"variation",
|
||||
"subevent",
|
||||
@@ -84,7 +125,6 @@ class CartPositionSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class CheckoutSessionSerializer(serializers.ModelSerializer):
|
||||
cart_positions = CartPositionSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = CheckoutSession
|
||||
@@ -92,9 +132,52 @@ class CheckoutSessionSerializer(serializers.ModelSerializer):
|
||||
"cart_id",
|
||||
"sales_channel",
|
||||
"testmode",
|
||||
"cart_positions",
|
||||
]
|
||||
|
||||
def to_representation(self, checkout):
|
||||
d = super().to_representation(checkout)
|
||||
|
||||
cartpos = CartPosition.objects.filter(
|
||||
event_id=self.context["event"], cart_id=checkout.cart_id
|
||||
)
|
||||
total = sum(p.price for p in cartpos)
|
||||
|
||||
try:
|
||||
fees = get_fees(
|
||||
self.context["event"],
|
||||
self.context["request"],
|
||||
total,
|
||||
(
|
||||
checkout.invoice_address
|
||||
if hasattr(checkout, "invoice_address")
|
||||
else None
|
||||
),
|
||||
payments=[], # todo
|
||||
positions=cartpos,
|
||||
)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
# ignore for now, will fail on order creation
|
||||
fees = []
|
||||
|
||||
total += sum([f.value for f in fees])
|
||||
d["cart_positions"] = CartPositionSerializer(
|
||||
sorted(cartpos, key=lambda c: c.sort_key), many=True
|
||||
).data
|
||||
d["cart_fees"] = CartFeeSerializer(fees, many=True).data
|
||||
d["total"] = str(total)
|
||||
|
||||
steps = get_steps(self.context["event"], cartpos)
|
||||
d["steps"] = {}
|
||||
for step in steps:
|
||||
applicable = step.is_applicable()
|
||||
valid = not applicable or step.is_valid()
|
||||
d["steps"][step.identifier] = {
|
||||
"applicable": applicable,
|
||||
"valid": valid,
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class CheckoutViewSet(viewsets.ViewSet):
|
||||
queryset = CheckoutSession.objects.none()
|
||||
@@ -109,6 +192,7 @@ class CheckoutViewSet(viewsets.ViewSet):
|
||||
instance=cs,
|
||||
context={
|
||||
"event": self.request.event,
|
||||
"request": self.request,
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
@@ -140,6 +224,82 @@ class CheckoutViewSet(viewsets.ViewSet):
|
||||
)
|
||||
return self._return_checkout_status(cs, status=200)
|
||||
|
||||
@action(detail=True, methods=["GET", "PUT"])
|
||||
def addons(self, request, *args, **kwargs):
|
||||
cs = get_object_or_404(
|
||||
self.request.event.checkout_sessions, cart_id=kwargs["cart_id"]
|
||||
)
|
||||
groups = get_addon_groups(
|
||||
self.request.event,
|
||||
self.request.sales_channel,
|
||||
cs.customer,
|
||||
CartPosition.objects.filter(cart_id=cs.cart_id),
|
||||
)
|
||||
ctx = {
|
||||
"event": self.request.event,
|
||||
}
|
||||
|
||||
if request.method == "PUT":
|
||||
serializer = CartAddonLineSerializer(
|
||||
data=request.data.get("lines", []),
|
||||
many=True,
|
||||
context={
|
||||
"event": self.request.event,
|
||||
"cart_id": cs.cart_id,
|
||||
},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# todo: early validation, validate_cart_addons?
|
||||
return self._do_async(
|
||||
cs,
|
||||
set_cart_addons,
|
||||
self.request.event.pk,
|
||||
serializer.validated_data,
|
||||
[],
|
||||
cs.cart_id,
|
||||
locale=translation.get_language(),
|
||||
invoice_address=(
|
||||
cs.invoice_address.pk if hasattr(cs, "invoice_address") else None
|
||||
),
|
||||
sales_channel=cs.sales_channel.identifier,
|
||||
override_now_dt=time_machine_now(default=None),
|
||||
)
|
||||
elif request.method == "GET":
|
||||
data = [
|
||||
{
|
||||
"parent": CartPositionSerializer(grp["pos"], context=ctx).data,
|
||||
"categories": [
|
||||
{
|
||||
"category": CategorySerializer(
|
||||
cat["category"], context=ctx
|
||||
).data,
|
||||
"multi_allowed": cat["multi_allowed"],
|
||||
"min_count": cat["min_count"],
|
||||
"max_count": cat["max_count"],
|
||||
"items": ItemSerializer(
|
||||
cat["items"],
|
||||
many=True,
|
||||
context={
|
||||
**ctx,
|
||||
"price_included": cat["price_included"],
|
||||
"max_count": (
|
||||
cat["max_count"] if cat["multi_allowed"] else 1
|
||||
),
|
||||
},
|
||||
).data,
|
||||
}
|
||||
for cat in grp["categories"]
|
||||
],
|
||||
}
|
||||
for grp in groups
|
||||
]
|
||||
return Response(
|
||||
data={
|
||||
"groups": data,
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["POST"])
|
||||
def add_to_cart(self, request, *args, **kwargs):
|
||||
cs = get_object_or_404(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework.generics import get_object_or_404
|
||||
@@ -6,6 +8,7 @@ from rest_framework.response import Response
|
||||
from pretix.base.models import (
|
||||
Event, Item, ItemCategory, ItemVariation, Quota, SubEvent,
|
||||
)
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.storelogic.products import (
|
||||
get_items_for_product_list, item_group_by_category,
|
||||
)
|
||||
@@ -86,20 +89,32 @@ class PricingField(serializers.Field):
|
||||
return None
|
||||
|
||||
item = item_or_var if isinstance(item_or_var, Item) else item_or_var.item
|
||||
suggested_price = item.suggested_price
|
||||
display_price = item.display_price
|
||||
|
||||
if self.context.get("price_included"):
|
||||
display_price = TaxedPrice(
|
||||
gross=Decimal("0.00"),
|
||||
net=Decimal("0.00"),
|
||||
tax=Decimal("0.00"),
|
||||
rate=Decimal("0.00"),
|
||||
name="",
|
||||
code=None,
|
||||
)
|
||||
|
||||
if hasattr(item, "initial_price"):
|
||||
# Pre-select current price for add-ons
|
||||
suggested_price = item.initial_price
|
||||
|
||||
return {
|
||||
"display_price": {
|
||||
"net": opt_str(item_or_var.display_price.net),
|
||||
"gross": opt_str(item_or_var.display_price.gross),
|
||||
"net": opt_str(display_price.net),
|
||||
"gross": opt_str(display_price.gross),
|
||||
"tax_rate": opt_str(
|
||||
item_or_var.display_price.rate
|
||||
if not item.includes_mixed_tax_rate
|
||||
else None
|
||||
display_price.rate if not item.includes_mixed_tax_rate else None
|
||||
),
|
||||
"tax_name": opt_str(
|
||||
item_or_var.display_price.name
|
||||
if not item.includes_mixed_tax_rate
|
||||
else None
|
||||
display_price.name if not item.includes_mixed_tax_rate else None
|
||||
),
|
||||
},
|
||||
"original_price": (
|
||||
@@ -122,20 +137,16 @@ class PricingField(serializers.Field):
|
||||
),
|
||||
"free_price": item.free_price,
|
||||
"suggested_price": {
|
||||
"net": opt_str(item_or_var.suggested_price.net),
|
||||
"gross": opt_str(item_or_var.suggested_price.gross),
|
||||
"net": opt_str(suggested_price.net),
|
||||
"gross": opt_str(suggested_price.gross),
|
||||
"tax_rate": opt_str(
|
||||
item_or_var.suggested_price.rate
|
||||
if not item.includes_mixed_tax_rate
|
||||
else None
|
||||
suggested_price.rate if not item.includes_mixed_tax_rate else None
|
||||
),
|
||||
"tax_name": opt_str(
|
||||
item_or_var.suggested_price.name
|
||||
if not item.includes_mixed_tax_rate
|
||||
else None
|
||||
suggested_price.name if not item.includes_mixed_tax_rate else None
|
||||
),
|
||||
},
|
||||
"mandatory_priced_addons": item.mandatory_priced_addons,
|
||||
"mandatory_priced_addons": getattr(item, "mandatory_priced_addons", False),
|
||||
"includes_mixed_tax_rate": item.includes_mixed_tax_rate,
|
||||
}
|
||||
|
||||
@@ -211,7 +222,7 @@ class AvailabilityField(serializers.Field):
|
||||
"code": "ok",
|
||||
"message": None,
|
||||
"waiting_list": False,
|
||||
"max_selection": item_or_var.order_max,
|
||||
"max_selection": self.context.get("max_count", item_or_var.order_max),
|
||||
"quota_left": (
|
||||
item_or_var.cached_availability[1]
|
||||
if item.show_quota_left
|
||||
@@ -236,6 +247,13 @@ class VariationSerializer(I18nFlattenedModelSerializer):
|
||||
"availability",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
if hasattr(instance, "initial"):
|
||||
# Used for addons
|
||||
r["initial_count"] = instance.initial
|
||||
return r
|
||||
|
||||
|
||||
class ItemSerializer(I18nFlattenedModelSerializer):
|
||||
description = RichTextField()
|
||||
@@ -258,6 +276,13 @@ class ItemSerializer(I18nFlattenedModelSerializer):
|
||||
"availability",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
if hasattr(instance, "initial"):
|
||||
# Used for addons
|
||||
r["initial_count"] = instance.initial
|
||||
return r
|
||||
|
||||
|
||||
class ProductGroupField(serializers.Field):
|
||||
def to_representation(self, ev):
|
||||
|
||||
@@ -129,7 +129,17 @@ class ApiMiddleware:
|
||||
LocaleMiddleware(NotImplementedError).process_request(request)
|
||||
r = self.get_response(request)
|
||||
r["Access-Control-Allow-Origin"] = "*" # todo: allow whitelist?
|
||||
r["Access-Control-Allow-Headers"] = ",".join(
|
||||
r["Access-Control-Allow-Methods"] = ", ".join(
|
||||
[
|
||||
"GET",
|
||||
"POST",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
]
|
||||
)
|
||||
r["Access-Control-Allow-Headers"] = ", ".join(
|
||||
[
|
||||
"Content-Type",
|
||||
"X-Storefront-Time-Machine-Date",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from pretix.base.storelogic.addons import (
|
||||
addons_is_applicable, addons_is_completed,
|
||||
)
|
||||
|
||||
|
||||
class CheckoutStep:
|
||||
def __init__(self, event, cart_positions):
|
||||
self.event = event
|
||||
self.cart_positions = cart_positions
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_applicable(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_valid(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AddonStep(CheckoutStep):
|
||||
identifier = "addons"
|
||||
|
||||
def is_applicable(self):
|
||||
return addons_is_applicable(self.cart_positions)
|
||||
|
||||
def is_valid(self):
|
||||
return addons_is_completed(self.cart_positions)
|
||||
|
||||
|
||||
def get_steps(event, cart_positions):
|
||||
return [
|
||||
AddonStep(event, cart_positions),
|
||||
# todo: cross-selling
|
||||
# todo: customers
|
||||
# todo: memberships
|
||||
# todo: questions
|
||||
# todo: plugin signals
|
||||
# todo: payment
|
||||
# todo: confirmations
|
||||
]
|
||||
Reference in New Issue
Block a user