mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
API: Allow to change orders (#2552)
This commit is contained in:
@@ -424,88 +424,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
self.fields.pop('pdf_data', None)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = [
|
||||
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'attendee_email',
|
||||
]
|
||||
answers_data = validated_data.pop('answers', None)
|
||||
|
||||
name = validated_data.pop('attendee_name', '')
|
||||
if name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in update_fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=update_fields)
|
||||
|
||||
if answers_data is not None:
|
||||
qs_seen = set()
|
||||
answercache = {
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
a = answercache[answ_data['question'].pk]
|
||||
if isinstance(answ_data['answer'], File):
|
||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
||||
pass # keep current file
|
||||
else:
|
||||
for attr, value in answ_data.items():
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(os.path.basename(an.name), an, save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
a.save()
|
||||
else:
|
||||
a = instance.answers.create(**answ_data)
|
||||
a.options.set(options)
|
||||
qs_seen.add(a.question_id)
|
||||
for qid, a in answercache.items():
|
||||
if qid not in qs_seen:
|
||||
a.delete()
|
||||
|
||||
return instance
|
||||
raise TypeError("this serializer is readonly")
|
||||
|
||||
|
||||
class RequireAttentionField(serializers.Field):
|
||||
@@ -593,7 +512,7 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
@@ -1361,14 +1280,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
if not simulate:
|
||||
if simulate:
|
||||
f.id = 0
|
||||
else:
|
||||
f.save()
|
||||
else:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
if not simulate:
|
||||
if simulate:
|
||||
f.id = 0
|
||||
else:
|
||||
f.save()
|
||||
|
||||
order.total += sum([f.value for f in fees])
|
||||
|
||||
424
src/pretix/api/serializers/orderchange.py
Normal file
424
src/pretix/api/serializers/orderchange.py
Normal file
@@ -0,0 +1,424 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pycountry
|
||||
from django.core.files import File
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
|
||||
OrderPositionCreateSerializer,
|
||||
)
|
||||
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||
from pretix.base.services.orders import OrderError
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False)
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||
max_digits=10)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('order', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['order'].queryset = self.context['event'].orders.all()
|
||||
self.fields['item'].queryset = self.context['event'].items.all()
|
||||
self.fields['subevent'].queryset = self.context['event'].subevents.all()
|
||||
self.fields['seat'].queryset = self.context['event'].seats.all()
|
||||
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=self.context['event'])
|
||||
if 'order' in self.context:
|
||||
del self.fields['order']
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if data.get('addon_to'):
|
||||
try:
|
||||
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
raise ValidationError({
|
||||
'addon_to': ['addon_to refers to an unknown position ID for this order.']
|
||||
})
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
|
||||
try:
|
||||
ocm.add_position(
|
||||
item=validated_data['item'],
|
||||
variation=validated_data.get('variation'),
|
||||
price=validated_data.get('price'),
|
||||
addon_to=validated_data.get('addon_to'),
|
||||
subevent=validated_data.get('subevent'),
|
||||
seat=validated_data.get('seat'),
|
||||
)
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
return validated_data['order'].positions.order_by('-positionid').first()
|
||||
else:
|
||||
return OrderPosition() # fake to appease DRF
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = (
|
||||
'attendee_name', 'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'attendee_email', 'answers',
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
answers_data = validated_data.pop('answers', None)
|
||||
|
||||
name = validated_data.pop('attendee_name', '')
|
||||
if name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in self.fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=list(validated_data.keys()))
|
||||
|
||||
if answers_data is not None:
|
||||
qs_seen = set()
|
||||
answercache = {
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
a = answercache[answ_data['question'].pk]
|
||||
if isinstance(answ_data['answer'], File):
|
||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
||||
pass # keep current file
|
||||
else:
|
||||
for attr, value in answ_data.items():
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(os.path.basename(an.name), an, save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
a.save()
|
||||
else:
|
||||
a = instance.answers.create(**answ_data)
|
||||
a.options.set(options)
|
||||
qs_seen.add(a.question_id)
|
||||
for qid, a in answercache.items():
|
||||
if qid not in qs_seen:
|
||||
a.delete()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
seat = serializers.CharField(source='seat.seat_guid', allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = (
|
||||
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['item'].queryset = self.context['event'].items.all()
|
||||
self.fields['subevent'].queryset = self.context['event'].subevents.all()
|
||||
self.fields['tax_rule'].queryset = self.context['event'].tax_rules.all()
|
||||
if kwargs.get('partial'):
|
||||
for k, v in self.fields.items():
|
||||
self.fields[k].required = False
|
||||
|
||||
def validate_item(self, item):
|
||||
if item.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified item does not belong to this event.'
|
||||
)
|
||||
return item
|
||||
|
||||
def validate_subevent(self, subevent):
|
||||
if self.context['event'].has_subevents:
|
||||
if not subevent:
|
||||
raise ValidationError(
|
||||
'You need to set a subevent.'
|
||||
)
|
||||
if subevent.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified subevent does not belong to this event.'
|
||||
)
|
||||
elif subevent:
|
||||
raise ValidationError(
|
||||
'You cannot set a subevent for this event.'
|
||||
)
|
||||
return subevent
|
||||
|
||||
def validate(self, data, instance=None):
|
||||
instance = instance or self.instance
|
||||
if instance is None:
|
||||
return data # needs to be done later
|
||||
if data.get('item', instance.item):
|
||||
if data.get('item', instance.item).has_variations:
|
||||
if not data.get('variation', instance.variation):
|
||||
raise ValidationError({'variation': ['You should specify a variation for this item.']})
|
||||
else:
|
||||
if data.get('variation', instance.variation).item != data.get('item', instance.item):
|
||||
raise ValidationError(
|
||||
{'variation': ['The specified variation does not belong to the specified item.']}
|
||||
)
|
||||
elif data.get('variation', instance.variation):
|
||||
raise ValidationError(
|
||||
{'variation': ['You cannot specify a variation for this item.']}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||
item = validated_data.get('item', instance.item)
|
||||
variation = validated_data.get('variation', instance.variation)
|
||||
subevent = validated_data.get('subevent', instance.subevent)
|
||||
price = validated_data.get('price', instance.price)
|
||||
seat = validated_data.get('seat', current_seat)
|
||||
tax_rule = validated_data.get('tax_rule', instance.tax_rule)
|
||||
|
||||
change_item = None
|
||||
if item != instance.item or variation != instance.variation:
|
||||
change_item = (item, variation)
|
||||
|
||||
change_subevent = None
|
||||
if self.context['event'].has_subevents and subevent != instance.subevent:
|
||||
change_subevent = (subevent,)
|
||||
|
||||
try:
|
||||
if change_item is not None and change_subevent is not None:
|
||||
ocm.change_item_and_subevent(instance, *change_item, *change_subevent)
|
||||
elif change_item is not None:
|
||||
ocm.change_item(instance, *change_item)
|
||||
elif change_subevent is not None:
|
||||
ocm.change_subevent(instance, *change_subevent)
|
||||
|
||||
if seat != current_seat or change_subevent:
|
||||
ocm.change_seat(instance, seat['seat_guid'] if seat else None)
|
||||
|
||||
if price != instance.price:
|
||||
ocm.change_price(instance, price)
|
||||
|
||||
if tax_rule != instance.tax_rule:
|
||||
ocm.change_tax_rule(instance, tax_rule)
|
||||
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
instance.refresh_from_db()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
return instance
|
||||
|
||||
|
||||
class PatchPositionSerializer(serializers.Serializer):
|
||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none())
|
||||
|
||||
def validate_position(self, value):
|
||||
self.fields['body'].instance = value # hack around DRFs validation order
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
OrderPositionChangeSerializer(context=self.context, partial=True).validate(data['body'], data['position'])
|
||||
return data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['position'].queryset = self.context['order'].positions.all()
|
||||
self.fields['body'] = OrderPositionChangeSerializer(context=self.context, partial=True)
|
||||
|
||||
|
||||
class SelectPositionSerializer(serializers.Serializer):
|
||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['position'].queryset = self.context['order'].positions.all()
|
||||
|
||||
|
||||
class OrderFeeChangeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = (
|
||||
'value',
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
value = validated_data.get('value', instance.value)
|
||||
|
||||
try:
|
||||
if value != instance.value:
|
||||
ocm.change_fee(instance, value)
|
||||
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
instance.refresh_from_db()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
return instance
|
||||
|
||||
|
||||
class PatchFeeSerializer(serializers.Serializer):
|
||||
fee = serializers.PrimaryKeyRelatedField(queryset=OrderFee.all.none())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['fee'].queryset = self.context['order'].fees.all()
|
||||
self.fields['body'] = OrderFeeChangeSerializer(context=self.context)
|
||||
|
||||
|
||||
class SelectFeeSerializer(serializers.Serializer):
|
||||
fee = serializers.PrimaryKeyRelatedField(queryset=OrderFee.all.none())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['fee'].queryset = self.context['order'].fees.all()
|
||||
|
||||
|
||||
class OrderChangeOperationSerializer(serializers.Serializer):
|
||||
send_email = serializers.BooleanField(default=False, required=False)
|
||||
reissue_invoice = serializers.BooleanField(default=True, required=False)
|
||||
recalculate_taxes = serializers.ChoiceField(default=None, allow_null=True, required=False, choices=[
|
||||
('keep_net', 'keep_net'),
|
||||
('keep_gross', 'keep_gross'),
|
||||
])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(self, *args, **kwargs)
|
||||
self.fields['patch_positions'] = PatchPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['cancel_positions'] = SelectPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['create_positions'] = OrderPositionCreateForExistingOrderSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['split_positions'] = SelectPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['patch_fees'] = PatchFeeSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['cancel_fees'] = SelectFeeSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
seen_positions = set()
|
||||
for d in data.get('patch_positions', []):
|
||||
print(d, seen_positions)
|
||||
if d['position'] in seen_positions:
|
||||
raise ValidationError({'patch_positions': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['position'])
|
||||
seen_positions = set()
|
||||
for d in data.get('cancel_positions', []):
|
||||
if d['position'] in seen_positions:
|
||||
raise ValidationError({'cancel_positions': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['position'])
|
||||
seen_positions = set()
|
||||
for d in data.get('split_positions', []):
|
||||
if d['position'] in seen_positions:
|
||||
raise ValidationError({'split_positions': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['position'])
|
||||
seen_fees = set()
|
||||
for d in data.get('patch_fees', []):
|
||||
if d['fee'] in seen_fees:
|
||||
raise ValidationError({'patch_fees': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['fee'])
|
||||
seen_fees = set()
|
||||
for d in data.get('cancel_fees', []):
|
||||
if d['fee'] in seen_fees:
|
||||
raise ValidationError({'cancel_fees': ['You have specified the same object twice.']})
|
||||
seen_positions.add(d['fee'])
|
||||
|
||||
return data
|
||||
@@ -36,7 +36,7 @@ from django.utils.translation import gettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from PIL import Image
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework import serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
APIException, NotFound, PermissionDenied, ValidationError,
|
||||
@@ -53,6 +53,12 @@ from pretix.api.serializers.order import (
|
||||
PriceCalcSerializer, RevokedTicketSecretSerializer,
|
||||
SimulatedOrderSerializer,
|
||||
)
|
||||
from pretix.api.serializers.orderchange import (
|
||||
OrderChangeOperationSerializer, OrderFeeChangeSerializer,
|
||||
OrderPositionChangeSerializer,
|
||||
OrderPositionCreateForExistingOrderSerializer,
|
||||
OrderPositionInfoPatchSerializer,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
|
||||
@@ -782,6 +788,79 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
with transaction.atomic():
|
||||
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def change(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
|
||||
serializer = OrderChangeOperationSerializer(
|
||||
context={'order': order, **self.get_serializer_context()},
|
||||
data=request.data,
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
try:
|
||||
ocm = OrderChangeManager(
|
||||
order=order,
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
notify=serializer.validated_data.get('send_email', False),
|
||||
reissue_invoice=serializer.validated_data.get('reissue_invoice', True),
|
||||
)
|
||||
|
||||
canceled_positions = set()
|
||||
for r in serializer.validated_data.get('cancel_positions', []):
|
||||
ocm.cancel(r['position'])
|
||||
canceled_positions.add(r['position'])
|
||||
|
||||
for r in serializer.validated_data.get('patch_positions', []):
|
||||
if r['position'] in canceled_positions:
|
||||
continue
|
||||
pos_serializer = OrderPositionChangeSerializer(
|
||||
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||
partial=True,
|
||||
)
|
||||
pos_serializer.update(r['position'], r['body'])
|
||||
|
||||
for r in serializer.validated_data.get('split_positions', []):
|
||||
if r['position'] in canceled_positions:
|
||||
continue
|
||||
ocm.split(r['position'])
|
||||
|
||||
for r in serializer.validated_data.get('create_positions', []):
|
||||
pos_serializer = OrderPositionCreateForExistingOrderSerializer(
|
||||
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||
)
|
||||
pos_serializer.create(r)
|
||||
|
||||
canceled_fees = set()
|
||||
for r in serializer.validated_data.get('cancel_fees', []):
|
||||
ocm.cancel_fee(r['fee'])
|
||||
canceled_fees.add(r['fee'])
|
||||
|
||||
for r in serializer.validated_data.get('patch_fees', []):
|
||||
if r['fee'] in canceled_fees:
|
||||
continue
|
||||
pos_serializer = OrderFeeChangeSerializer(
|
||||
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||
)
|
||||
pos_serializer.update(r['fee'], r['body'])
|
||||
|
||||
if serializer.validated_data.get('recalculate_taxes') == 'keep_net':
|
||||
ocm.recalculate_taxes(keep='net')
|
||||
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
||||
ocm.recalculate_taxes(keep='gross')
|
||||
|
||||
ocm.commit()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
order.refresh_from_db()
|
||||
serializer = OrderSerializer(
|
||||
instance=order,
|
||||
context=self.get_serializer_context(),
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class OrderPositionFilter(FilterSet):
|
||||
@@ -823,7 +902,7 @@ with scopes_disabled():
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -1060,6 +1139,25 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def regenerate_secrets(self, request, **kwargs):
|
||||
instance = self.get_object()
|
||||
try:
|
||||
ocm = OrderChangeManager(
|
||||
instance.order,
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=self.request.auth,
|
||||
notify=False,
|
||||
reissue_invoice=False,
|
||||
)
|
||||
ocm.regenerate_secret(instance)
|
||||
ocm.commit()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise ValidationError(str(e))
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
try:
|
||||
ocm = OrderChangeManager(
|
||||
@@ -1075,18 +1173,33 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.get('partial', False)
|
||||
if not partial:
|
||||
return Response(
|
||||
{"detail": "Method \"PUT\" not allowed."},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
def create(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
old_data = self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data
|
||||
serializer = OrderPositionCreateForExistingOrderSerializer(
|
||||
data=request.data,
|
||||
context=self.get_serializer_context(),
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
order = serializer.validated_data['order']
|
||||
ocm = OrderChangeManager(
|
||||
order=order,
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
notify=False,
|
||||
reissue_invoice=False,
|
||||
)
|
||||
serializer.context['ocm'] = ocm
|
||||
serializer.save()
|
||||
|
||||
# Fields that can be easily patched after the position was added
|
||||
old_data = OrderPositionInfoPatchSerializer(instance=serializer.instance, context=self.get_serializer_context()).data
|
||||
serializer = OrderPositionInfoPatchSerializer(
|
||||
instance=serializer.instance,
|
||||
context=self.get_serializer_context(),
|
||||
partial=True,
|
||||
data=request.data
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
new_data = serializer.data
|
||||
|
||||
@@ -1109,9 +1222,77 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
]
|
||||
}
|
||||
)
|
||||
tickets.invalidate_cache.apply_async(
|
||||
kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||
return Response(
|
||||
OrderPositionSerializer(serializer.instance, context=self.get_serializer_context()).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.get('partial', False)
|
||||
if not partial:
|
||||
return Response(
|
||||
{"detail": "Method \"PUT\" not allowed."},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
instance = self.get_object()
|
||||
ocm = OrderChangeManager(
|
||||
order=instance.order,
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
notify=False,
|
||||
reissue_invoice=False,
|
||||
)
|
||||
|
||||
# Field that need to go through OrderChangeManager
|
||||
serializer = OrderPositionChangeSerializer(
|
||||
instance=instance,
|
||||
context={'ocm': ocm, **self.get_serializer_context()},
|
||||
partial=True,
|
||||
data=request.data
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# Fields that can be easily patched
|
||||
old_data = OrderPositionInfoPatchSerializer(instance=instance, context=self.get_serializer_context()).data
|
||||
serializer = OrderPositionInfoPatchSerializer(
|
||||
instance=instance,
|
||||
context=self.get_serializer_context(),
|
||||
partial=True,
|
||||
data=request.data
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
new_data = serializer.data
|
||||
|
||||
if old_data != new_data:
|
||||
log_data = self.request.data
|
||||
if 'answers' in log_data:
|
||||
for a in new_data['answers']:
|
||||
log_data[f'question_{a["question"]}'] = a["answer"]
|
||||
log_data.pop('answers', None)
|
||||
serializer.instance.order.log_action(
|
||||
'pretix.event.order.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'data': [
|
||||
dict(
|
||||
position=serializer.instance.pk,
|
||||
**log_data
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||
|
||||
return Response(self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data)
|
||||
|
||||
|
||||
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
Reference in New Issue
Block a user