forked from CGM_Public/pretix_original
* Change semantics of changing orders This basically does two things to the "Change products" view of orders and the OrderChangeManager program API: 1) It decouples changing items or subevents from changing prices. OrderChangeManager.change_item() and .change_subevent() no longer touch the price of a position. Instead .change_price() needs to be called explicitly. However, a client-side JavaScript component now *proposes* a new price based on the changed item or subevent. 2) The user interface now exposes the possibility of doing multiple things at the same time, i.e. changing the item, subevent and price in the same operation. OrderChangeManager already allowed this before. (1) is basically a consequence of (2), while (2) is a prerequesite for e.g. the `seating` branch, where changing the subevent will always require changing the seat. * Add tests for price calculation API
775 lines
32 KiB
Python
775 lines
32 KiB
Python
import json
|
|
from collections import Counter
|
|
from decimal import Decimal
|
|
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import ugettext_lazy
|
|
from django_countries.fields import Country
|
|
from rest_framework import serializers
|
|
from rest_framework.exceptions import ValidationError
|
|
from rest_framework.relations import SlugRelatedField
|
|
from rest_framework.reverse import reverse
|
|
|
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|
from pretix.base.channels import get_all_sales_channels
|
|
from pretix.base.i18n import language
|
|
from pretix.base.models import (
|
|
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
|
OrderPosition, Question, QuestionAnswer, SubEvent,
|
|
)
|
|
from pretix.base.models.orders import (
|
|
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
|
)
|
|
from pretix.base.pdf import get_variables
|
|
from pretix.base.signals import register_ticket_outputs
|
|
|
|
|
|
class CompatibleCountryField(serializers.Field):
|
|
def to_internal_value(self, data):
|
|
return {self.field_name: Country(data)}
|
|
|
|
def to_representation(self, instance: InvoiceAddress):
|
|
if instance.country:
|
|
return str(instance.country)
|
|
else:
|
|
return instance.country_old
|
|
|
|
|
|
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|
country = CompatibleCountryField(source='*')
|
|
name = serializers.CharField(required=False)
|
|
|
|
class Meta:
|
|
model = InvoiceAddress
|
|
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
|
'vat_id', 'vat_id_validated', 'internal_reference')
|
|
read_only_fields = ('last_modified', 'vat_id_validated')
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for v in self.fields.values():
|
|
v.required = False
|
|
v.allow_blank = True
|
|
|
|
def validate(self, data):
|
|
if data.get('name') and data.get('name_parts'):
|
|
raise ValidationError(
|
|
{'name': ['Do not specify name if you specified name_parts.']}
|
|
)
|
|
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
|
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
|
return data
|
|
|
|
|
|
class AnswerQuestionIdentifierField(serializers.Field):
|
|
def to_representation(self, instance: QuestionAnswer):
|
|
return instance.question.identifier
|
|
|
|
|
|
class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
|
def to_representation(self, instance: QuestionAnswer):
|
|
return [o.identifier for o in instance.options.all()]
|
|
|
|
|
|
class AnswerSerializer(I18nAwareModelSerializer):
|
|
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
|
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
|
|
|
class Meta:
|
|
model = QuestionAnswer
|
|
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
|
|
|
|
|
|
class CheckinSerializer(I18nAwareModelSerializer):
|
|
class Meta:
|
|
model = Checkin
|
|
fields = ('datetime', 'list')
|
|
|
|
|
|
class OrderDownloadsField(serializers.Field):
|
|
def to_representation(self, instance: Order):
|
|
if instance.status != Order.STATUS_PAID:
|
|
if instance.status != Order.STATUS_PENDING or instance.require_approval or not instance.event.settings.ticket_download_pending:
|
|
return []
|
|
|
|
request = self.context['request']
|
|
res = []
|
|
responses = register_ticket_outputs.send(instance.event)
|
|
for receiver, response in responses:
|
|
provider = response(instance.event)
|
|
if provider.is_enabled:
|
|
res.append({
|
|
'output': provider.identifier,
|
|
'url': reverse('api-v1:order-download', kwargs={
|
|
'organizer': instance.event.organizer.slug,
|
|
'event': instance.event.slug,
|
|
'code': instance.code,
|
|
'output': provider.identifier,
|
|
}, request=request)
|
|
})
|
|
return res
|
|
|
|
|
|
class PositionDownloadsField(serializers.Field):
|
|
def to_representation(self, instance: OrderPosition):
|
|
if instance.order.status != Order.STATUS_PAID:
|
|
if instance.order.status != Order.STATUS_PENDING or instance.order.require_approval or not instance.order.event.settings.ticket_download_pending:
|
|
return []
|
|
if not instance.generate_ticket:
|
|
return []
|
|
|
|
request = self.context['request']
|
|
res = []
|
|
responses = register_ticket_outputs.send(instance.order.event)
|
|
for receiver, response in responses:
|
|
provider = response(instance.order.event)
|
|
if provider.is_enabled:
|
|
res.append({
|
|
'output': provider.identifier,
|
|
'url': reverse('api-v1:orderposition-download', kwargs={
|
|
'organizer': instance.order.event.organizer.slug,
|
|
'event': instance.order.event.slug,
|
|
'pk': instance.pk,
|
|
'output': provider.identifier,
|
|
}, request=request)
|
|
})
|
|
return res
|
|
|
|
|
|
class PdfDataSerializer(serializers.Field):
|
|
def to_representation(self, instance: OrderPosition):
|
|
res = {}
|
|
|
|
ev = instance.subevent or instance.order.event
|
|
with language(instance.order.locale):
|
|
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
|
|
# we serialize a list.
|
|
|
|
if 'vars' not in self.context:
|
|
self.context['vars'] = get_variables(self.context['request'].event)
|
|
|
|
for k, f in self.context['vars'].items():
|
|
res[k] = f['evaluate'](instance, instance.order, ev)
|
|
|
|
if not hasattr(ev, '_cached_meta_data'):
|
|
ev._cached_meta_data = ev.meta_data
|
|
|
|
for k, v in ev._cached_meta_data.items():
|
|
res['meta:' + k] = v
|
|
|
|
return res
|
|
|
|
|
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
|
checkins = CheckinSerializer(many=True)
|
|
answers = AnswerSerializer(many=True)
|
|
downloads = PositionDownloadsField(source='*')
|
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
|
pdf_data = PdfDataSerializer(source='*')
|
|
|
|
class Meta:
|
|
model = OrderPosition
|
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
|
self.fields.pop('pdf_data')
|
|
|
|
|
|
class RequireAttentionField(serializers.Field):
|
|
def to_representation(self, instance: OrderPosition):
|
|
return instance.order.checkin_attention or instance.item.checkin_attention
|
|
|
|
|
|
class AttendeeNameField(serializers.Field):
|
|
def to_representation(self, instance: OrderPosition):
|
|
an = instance.attendee_name
|
|
if not an:
|
|
if instance.addon_to_id:
|
|
an = instance.addon_to.attendee_name
|
|
if not an:
|
|
try:
|
|
an = instance.order.invoice_address.name
|
|
except InvoiceAddress.DoesNotExist:
|
|
pass
|
|
return an
|
|
|
|
|
|
class AttendeeNamePartsField(serializers.Field):
|
|
def to_representation(self, instance: OrderPosition):
|
|
an = instance.attendee_name
|
|
p = instance.attendee_name_parts
|
|
if not an:
|
|
if instance.addon_to_id:
|
|
an = instance.addon_to.attendee_name
|
|
p = instance.addon_to.attendee_name_parts
|
|
if not an:
|
|
try:
|
|
p = instance.order.invoice_address.name_parts
|
|
except InvoiceAddress.DoesNotExist:
|
|
pass
|
|
return p
|
|
|
|
|
|
class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|
require_attention = RequireAttentionField(source='*')
|
|
attendee_name = AttendeeNameField(source='*')
|
|
attendee_name_parts = AttendeeNamePartsField(source='*')
|
|
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
|
|
|
class Meta:
|
|
model = OrderPosition
|
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
|
|
'order__status')
|
|
|
|
|
|
class OrderPaymentTypeField(serializers.Field):
|
|
# TODO: Remove after pretix 2.2
|
|
def to_representation(self, instance: Order):
|
|
t = None
|
|
for p in instance.payments.all():
|
|
t = p.provider
|
|
return t
|
|
|
|
|
|
class OrderPaymentDateField(serializers.DateField):
|
|
# TODO: Remove after pretix 2.2
|
|
def to_representation(self, instance: Order):
|
|
t = None
|
|
for p in instance.payments.all():
|
|
t = p.payment_date or t
|
|
if t:
|
|
|
|
return super().to_representation(t.date())
|
|
|
|
|
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
|
class Meta:
|
|
model = OrderFee
|
|
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
|
|
|
|
|
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
|
class Meta:
|
|
model = OrderPayment
|
|
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
|
|
|
|
|
|
class OrderRefundSerializer(I18nAwareModelSerializer):
|
|
payment = SlugRelatedField(slug_field='local_id', read_only=True)
|
|
|
|
class Meta:
|
|
model = OrderRefund
|
|
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
|
|
|
|
|
class OrderSerializer(I18nAwareModelSerializer):
|
|
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
|
positions = OrderPositionSerializer(many=True, read_only=True)
|
|
fees = OrderFeeSerializer(many=True, read_only=True)
|
|
downloads = OrderDownloadsField(source='*', read_only=True)
|
|
payments = OrderPaymentSerializer(many=True, read_only=True)
|
|
refunds = OrderRefundSerializer(many=True, read_only=True)
|
|
payment_date = OrderPaymentDateField(source='*', read_only=True)
|
|
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
|
|
|
class Meta:
|
|
model = Order
|
|
fields = (
|
|
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
|
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
|
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
|
)
|
|
read_only_fields = (
|
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
|
'payment_provider', 'fees', 'total', 'positions', 'downloads',
|
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
|
self.fields['positions'].child.fields.pop('pdf_data')
|
|
|
|
def validate_locale(self, l):
|
|
if l not in set(k for k in self.instance.event.settings.locales):
|
|
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
|
|
return l
|
|
|
|
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 = ['comment', 'checkin_attention', 'email', 'locale']
|
|
print(validated_data)
|
|
|
|
if 'invoice_address' in validated_data:
|
|
iadata = validated_data.pop('invoice_address')
|
|
|
|
if not iadata:
|
|
try:
|
|
instance.invoice_address.delete()
|
|
except InvoiceAddress.DoesNotExist:
|
|
pass
|
|
else:
|
|
name = iadata.pop('name', '')
|
|
if name and not iadata.get('name_parts'):
|
|
iadata['name_parts'] = {
|
|
'_legacy': name
|
|
}
|
|
try:
|
|
ia = instance.invoice_address
|
|
if iadata.get('vat_id') != ia.vat_id:
|
|
ia.vat_id_validated = False
|
|
self.fields['invoice_address'].update(ia, iadata)
|
|
except InvoiceAddress.DoesNotExist:
|
|
InvoiceAddress.objects.create(order=instance, **iadata)
|
|
|
|
for attr, value in validated_data.items():
|
|
if attr in update_fields:
|
|
setattr(instance, attr, value)
|
|
|
|
instance.save(update_fields=update_fields)
|
|
return instance
|
|
|
|
|
|
class PriceCalcSerializer(serializers.Serializer):
|
|
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
|
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
|
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
|
|
locale = serializers.CharField(allow_null=True, required=False)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
event = kwargs.pop('event')
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['item'].queryset = event.items.all()
|
|
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
|
|
if event.has_subevents:
|
|
self.fields['subevent'].queryset = event.subevents.all()
|
|
else:
|
|
del self.fields['subevent']
|
|
|
|
|
|
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
|
|
|
class Meta:
|
|
model = QuestionAnswer
|
|
fields = ('question', 'answer', 'options')
|
|
|
|
def validate_question(self, q):
|
|
if q.event != self.context['event']:
|
|
raise ValidationError(
|
|
'The specified question does not belong to this event.'
|
|
)
|
|
return q
|
|
|
|
def validate(self, data):
|
|
if data.get('question').type == Question.TYPE_FILE:
|
|
raise ValidationError(
|
|
'File uploads are currently not supported via the API.'
|
|
)
|
|
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
|
if not data.get('options'):
|
|
raise ValidationError(
|
|
'You need to specify options if the question is of a choice type.'
|
|
)
|
|
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
|
raise ValidationError(
|
|
'You can specify at most one option for this question.'
|
|
)
|
|
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
|
|
|
else:
|
|
if data.get('options'):
|
|
raise ValidationError(
|
|
'You should not specify options if the question is not of a choice type.'
|
|
)
|
|
|
|
if data.get('question').type == Question.TYPE_BOOLEAN:
|
|
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
|
data['answer'] = 'True'
|
|
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
|
data['answer'] = 'False'
|
|
else:
|
|
raise ValidationError(
|
|
'Please specify "true" or "false" for boolean questions.'
|
|
)
|
|
elif data.get('question').type == Question.TYPE_NUMBER:
|
|
serializers.DecimalField(
|
|
max_digits=50,
|
|
decimal_places=25
|
|
).to_internal_value(data.get('answer'))
|
|
elif data.get('question').type == Question.TYPE_DATE:
|
|
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
|
elif data.get('question').type == Question.TYPE_TIME:
|
|
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
|
elif data.get('question').type == Question.TYPE_DATETIME:
|
|
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
|
return data
|
|
|
|
|
|
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
|
class Meta:
|
|
model = OrderFee
|
|
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
|
|
|
|
def validate_tax_rule(self, tr):
|
|
if tr and tr.event != self.context['event']:
|
|
raise ValidationError(
|
|
'The specified tax rate does not belong to this event.'
|
|
)
|
|
return tr
|
|
|
|
|
|
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|
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)
|
|
|
|
class Meta:
|
|
model = OrderPosition
|
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
|
'secret', 'addon_to', 'subevent', 'answers')
|
|
|
|
def validate_secret(self, secret):
|
|
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
|
raise ValidationError(
|
|
'You cannot assign a position secret that already exists.'
|
|
)
|
|
return secret
|
|
|
|
def validate_item(self, item):
|
|
if item.event != self.context['event']:
|
|
raise ValidationError(
|
|
'The specified item does not belong to this event.'
|
|
)
|
|
if not item.active:
|
|
raise ValidationError(
|
|
'The specified item is not active.'
|
|
)
|
|
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):
|
|
if data.get('item'):
|
|
if data.get('item').has_variations:
|
|
if not data.get('variation'):
|
|
raise ValidationError({'variation': ['You should specify a variation for this item.']})
|
|
else:
|
|
if data.get('variation').item != data.get('item'):
|
|
raise ValidationError(
|
|
{'variation': ['The specified variation does not belong to the specified item.']}
|
|
)
|
|
elif data.get('variation'):
|
|
raise ValidationError(
|
|
{'variation': ['You cannot specify a variation for this item.']}
|
|
)
|
|
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
|
|
return data
|
|
|
|
|
|
class CompatibleJSONField(serializers.JSONField):
|
|
def to_internal_value(self, data):
|
|
try:
|
|
return json.dumps(data)
|
|
except (TypeError, ValueError):
|
|
self.fail('invalid')
|
|
|
|
def to_representation(self, value):
|
|
if value:
|
|
return json.loads(value)
|
|
return value
|
|
|
|
|
|
class OrderCreateSerializer(I18nAwareModelSerializer):
|
|
invoice_address = InvoiceAddressSerializer(required=False)
|
|
positions = OrderPositionCreateSerializer(many=True, required=False)
|
|
fees = OrderFeeCreateSerializer(many=True, required=False)
|
|
status = serializers.ChoiceField(choices=(
|
|
('n', Order.STATUS_PENDING),
|
|
('p', Order.STATUS_PAID),
|
|
), default='n', required=False)
|
|
code = serializers.CharField(
|
|
required=False,
|
|
max_length=16,
|
|
min_length=5
|
|
)
|
|
comment = serializers.CharField(required=False, allow_blank=True)
|
|
payment_provider = serializers.CharField(required=True)
|
|
payment_info = CompatibleJSONField(required=False)
|
|
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
|
force = serializers.BooleanField(default=False, required=False)
|
|
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
|
|
|
class Meta:
|
|
model = Order
|
|
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
|
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
|
|
|
|
def validate_payment_provider(self, pp):
|
|
if pp not in self.context['event'].get_payment_providers():
|
|
raise ValidationError('The given payment provider is not known.')
|
|
return pp
|
|
|
|
def validate_sales_channel(self, channel):
|
|
if channel not in get_all_sales_channels():
|
|
raise ValidationError('Unknown sales channel.')
|
|
return channel
|
|
|
|
def validate_code(self, code):
|
|
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
|
raise ValidationError(
|
|
'This order code is already in use.'
|
|
)
|
|
if any(c not in 'ABCDEFGHJKLMNPQRSTUVWXYZ1234567890' for c in code):
|
|
raise ValidationError(
|
|
'This order code contains invalid characters.'
|
|
)
|
|
return code
|
|
|
|
def validate_positions(self, data):
|
|
if not data:
|
|
raise ValidationError(
|
|
'An order cannot be empty.'
|
|
)
|
|
errs = [{} for p in data]
|
|
if any([p.get('positionid') for p in data]):
|
|
if not all([p.get('positionid') for p in data]):
|
|
for i, p in enumerate(data):
|
|
if not p.get('positionid'):
|
|
errs[i]['positionid'] = [
|
|
'If you set position IDs manually, you need to do so for all positions.'
|
|
]
|
|
raise ValidationError(errs)
|
|
|
|
last_non_add_on = None
|
|
last_posid = 0
|
|
|
|
for i, p in enumerate(data):
|
|
if p['positionid'] != last_posid + 1:
|
|
errs[i]['positionid'] = [
|
|
'Position IDs need to be consecutive.'
|
|
]
|
|
if p.get('addon_to') and p['addon_to'] != last_non_add_on:
|
|
errs[i]['addon_to'] = [
|
|
"If you set addon_to, you need to make sure that the referenced "
|
|
"position ID exists and is transmitted directly before its add-ons."
|
|
]
|
|
|
|
if not p.get('addon_to'):
|
|
last_non_add_on = p['positionid']
|
|
last_posid = p['positionid']
|
|
|
|
elif any([p.get('addon_to') for p in data]):
|
|
errs = [
|
|
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
|
|
for p in data
|
|
]
|
|
|
|
if any(errs):
|
|
raise ValidationError(errs)
|
|
return data
|
|
|
|
def create(self, validated_data):
|
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
|
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
|
payment_provider = validated_data.pop('payment_provider')
|
|
payment_info = validated_data.pop('payment_info', '{}')
|
|
payment_date = validated_data.pop('payment_date', now())
|
|
force = validated_data.pop('force', False)
|
|
|
|
if 'invoice_address' in validated_data:
|
|
iadata = validated_data.pop('invoice_address')
|
|
name = iadata.pop('name', '')
|
|
if name and not iadata.get('name_parts'):
|
|
iadata['name_parts'] = {
|
|
'_legacy': name
|
|
}
|
|
ia = InvoiceAddress(**iadata)
|
|
else:
|
|
ia = None
|
|
|
|
with self.context['event'].lock() as now_dt:
|
|
quotadiff = Counter()
|
|
|
|
consume_carts = validated_data.pop('consume_carts', [])
|
|
delete_cps = []
|
|
quota_avail_cache = {}
|
|
if consume_carts:
|
|
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
|
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
|
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
|
for quota in quotas:
|
|
if quota not in quota_avail_cache:
|
|
quota_avail_cache[quota] = list(quota.availability())
|
|
if quota_avail_cache[quota][1] is not None:
|
|
quota_avail_cache[quota][1] += 1
|
|
if cp.expires > now_dt:
|
|
quotadiff.subtract(quotas)
|
|
delete_cps.append(cp)
|
|
|
|
errs = [{} for p in positions_data]
|
|
|
|
if not force:
|
|
for i, pos_data in enumerate(positions_data):
|
|
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
|
if pos_data.get('variation')
|
|
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
|
if len(new_quotas) == 0:
|
|
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
|
str(pos_data.get('item'))
|
|
)]
|
|
else:
|
|
for quota in new_quotas:
|
|
if quota not in quota_avail_cache:
|
|
quota_avail_cache[quota] = list(quota.availability())
|
|
|
|
if quota_avail_cache[quota][1] is not None:
|
|
quota_avail_cache[quota][1] -= 1
|
|
if quota_avail_cache[quota][1] < 0:
|
|
errs[i]['item'] = [
|
|
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
|
quota.name
|
|
)
|
|
]
|
|
|
|
quotadiff.update(new_quotas)
|
|
|
|
if any(errs):
|
|
raise ValidationError({'positions': errs})
|
|
|
|
if validated_data.get('locale', None) is None:
|
|
validated_data['locale'] = self.context['event'].settings.locale
|
|
order = Order(event=self.context['event'], **validated_data)
|
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
|
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
|
|
order.meta_info = "{}"
|
|
order.save()
|
|
|
|
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
|
order.status = Order.STATUS_PAID
|
|
order.save()
|
|
order.payments.create(
|
|
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
|
payment_date=now()
|
|
)
|
|
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
|
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
|
elif validated_data.get('status') == Order.STATUS_PAID:
|
|
order.payments.create(
|
|
amount=order.total,
|
|
provider=payment_provider,
|
|
info=payment_info,
|
|
payment_date=payment_date,
|
|
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
|
)
|
|
elif payment_provider:
|
|
order.payments.create(
|
|
amount=order.total,
|
|
provider=payment_provider,
|
|
info=payment_info,
|
|
state=OrderPayment.PAYMENT_STATE_CREATED
|
|
)
|
|
|
|
if ia:
|
|
ia.order = order
|
|
ia.save()
|
|
pos_map = {}
|
|
for pos_data in positions_data:
|
|
answers_data = pos_data.pop('answers', [])
|
|
addon_to = pos_data.pop('addon_to', None)
|
|
attendee_name = pos_data.pop('attendee_name', '')
|
|
if attendee_name and not pos_data.get('attendee_name_parts'):
|
|
pos_data['attendee_name_parts'] = {
|
|
'_legacy': attendee_name
|
|
}
|
|
pos = OrderPosition(**pos_data)
|
|
pos.order = order
|
|
pos._calculate_tax()
|
|
if addon_to:
|
|
pos.addon_to = pos_map[addon_to]
|
|
pos.save()
|
|
pos_map[pos.positionid] = pos
|
|
for answ_data in answers_data:
|
|
options = answ_data.pop('options', [])
|
|
answ = pos.answers.create(**answ_data)
|
|
answ.options.add(*options)
|
|
|
|
for cp in delete_cps:
|
|
cp.delete()
|
|
for fee_data in fees_data:
|
|
f = OrderFee(**fee_data)
|
|
f.order = order
|
|
f._calculate_tax()
|
|
f.save()
|
|
|
|
return order
|
|
|
|
|
|
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
|
class Meta:
|
|
model = InvoiceLine
|
|
fields = ('description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
|
|
|
|
|
class InvoiceSerializer(I18nAwareModelSerializer):
|
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
|
refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True)
|
|
lines = InlineInvoiceLineSerializer(many=True)
|
|
|
|
class Meta:
|
|
model = Invoice
|
|
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
|
|
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
|
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
|
'internal_reference')
|
|
|
|
|
|
class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
|
payment = serializers.IntegerField(required=False, allow_null=True)
|
|
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
|
info = CompatibleJSONField(required=False)
|
|
|
|
class Meta:
|
|
model = OrderRefund
|
|
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info')
|
|
|
|
def create(self, validated_data):
|
|
pid = validated_data.pop('payment', None)
|
|
if pid:
|
|
try:
|
|
p = self.context['order'].payments.get(local_id=pid)
|
|
except OrderPayment.DoesNotExist:
|
|
raise ValidationError('Unknown payment ID.')
|
|
else:
|
|
p = None
|
|
|
|
order = OrderRefund(order=self.context['order'], payment=p, **validated_data)
|
|
order.save()
|
|
return order
|