diff --git a/doc/api/resources/items.rst b/doc/api/resources/items.rst index a8d7470766..cebc10a3ac 100644 --- a/doc/api/resources/items.rst +++ b/doc/api/resources/items.rst @@ -64,6 +64,12 @@ original_price money (string) An original pri require_approval boolean If ``True``, orders with this product will need to be approved by the event organizer before they can be paid. +generate_tickets boolean If ``False``, tickets are never generated for this + product, regardless of other settings. If ``True``, + tickets are generated even if this is a + non-admission or add-on product, regardless of event + settings. If this is ``null``, regular ticketing + rules apply. has_variations boolean Shows whether or not this item has variations. variations list of objects A list with one object for each variation of this item. Can be empty. Only writable during creation, @@ -111,6 +117,10 @@ addons list of objects Definition of a The ``sales_channels`` attribute has been added. +.. versionchanged:: 2.4 + + The ``generate_tickets`` attribute has been added. + Notes ----- Please note that an item either always has variations or never has. Once created with variations the item can never @@ -174,6 +184,7 @@ Endpoints "max_per_order": null, "checkin_attention": false, "has_variations": false, + "generate_tickets": null, "require_approval": false, "variations": [ { @@ -256,6 +267,7 @@ Endpoints "require_voucher": false, "hide_without_voucher": false, "allow_cancel": true, + "generate_tickets": null, "min_per_order": null, "max_per_order": null, "checkin_attention": false, @@ -323,6 +335,7 @@ Endpoints "require_voucher": false, "hide_without_voucher": false, "allow_cancel": true, + "generate_tickets": null, "min_per_order": null, "max_per_order": null, "checkin_attention": false, @@ -379,6 +392,7 @@ Endpoints "allow_cancel": true, "min_per_order": null, "max_per_order": null, + "generate_tickets": null, "checkin_attention": false, "has_variations": true, "require_approval": false, @@ -462,6 +476,7 @@ Endpoints "available_until": null, "require_voucher": false, "hide_without_voucher": false, + "generate_tickets": null, "allow_cancel": true, "min_per_order": null, "max_per_order": null, diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index 79d4b30552..43ee660e5f 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -79,7 +79,7 @@ class ItemSerializer(I18nAwareModelSerializer): 'position', 'picture', 'available_from', 'available_until', 'require_voucher', 'hide_without_voucher', 'allow_cancel', 'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', - 'variations', 'addons', 'original_price', 'require_approval') + 'variations', 'addons', 'original_price', 'require_approval', 'generate_tickets') read_only_fields = ('has_variations', 'picture') def get_serializer_context(self): diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index e254700dc4..56c1e8d0b8 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -114,9 +114,7 @@ class PositionDownloadsField(serializers.Field): 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 instance.addon_to_id and not instance.order.event.settings.ticket_download_addons: - return [] - if not instance.item.admission and not instance.order.event.settings.ticket_download_nonadm: + if not instance.generate_ticket: return [] request = self.context['request'] diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 27b33a10e0..875a060cdf 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -456,10 +456,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS if pos.order.status != Order.STATUS_PAID: raise PermissionDenied("Downloads are not available for unpaid orders.") - if pos.addon_to_id and not request.event.settings.ticket_download_addons: - raise PermissionDenied("Downloads are not enabled for add-on products.") - if not pos.item.admission and not request.event.settings.ticket_download_nonadm: - raise PermissionDenied("Downloads are not enabled for non-admission products.") + if not pos.generate_ticket: + raise PermissionDenied("Downloads are not enabled for this product.") ct = CachedTicket.objects.filter( order_position=pos, provider=provider.identifier, file__isnull=False diff --git a/src/pretix/base/migrations/0108_auto_20190201_1527.py b/src/pretix/base/migrations/0108_auto_20190201_1527.py new file mode 100644 index 0000000000..ba0f257f62 --- /dev/null +++ b/src/pretix/base/migrations/0108_auto_20190201_1527.py @@ -0,0 +1,21 @@ +# Generated by Django 2.1.5 on 2019-02-01 15:27 + +from django.db import migrations, models +import django.db.models.deletion +import jsonfallback.fields +import pretix.base.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0107_auto_20190129_1337'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='generate_tickets', + field=models.NullBooleanField(verbose_name='Allow ticket download'), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 3f177eef94..d40a7d6b8d 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -287,6 +287,10 @@ class Item(LoggedModel): ), default=False ) + generate_tickets = models.NullBooleanField( + verbose_name=_("Generate tickets"), + blank=True, null=True, + ) position = models.IntegerField( default=0 ) diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 00d4ec6dad..a92cacb149 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -689,9 +689,7 @@ class Order(LockModel, LoggedModel): @property def positions_with_tickets(self): for op in self.positions.all(): - if op.addon_to_id and not self.event.settings.ticket_download_addons: - continue - if not op.item.admission and not self.event.settings.ticket_download_nonadm: + if not op.generate_ticket: continue yield op @@ -1579,6 +1577,15 @@ class OrderPosition(AbstractPosition): def sort_key(self): return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0 + @property + def generate_ticket(self): + if self.item.generate_tickets is not None: + return self.item.generate_tickets + return ( + (self.order.event.settings.ticket_download_addons or not self.addon_to_id) and + (self.event.settings.ticket_download_nonadm or self.item.admission) + ) + @classmethod def transform_cart_positions(cls, cp: List, order) -> list: from . import Voucher diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index de865148f7..29f7048968 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -297,6 +297,16 @@ class ItemCreateForm(I18nModelForm): ] +class TicketNullBooleanSelect(forms.NullBooleanSelect): + def __init__(self, attrs=None): + choices = ( + ('1', _('Choose automatically depending on event settings')), + ('2', _('Yes, if ticket generation is enabled in general')), + ('3', _('Never')), + ) + super(forms.NullBooleanSelect, self).__init__(attrs, choices) + + class ItemUpdateForm(I18nModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -340,6 +350,7 @@ class ItemUpdateForm(I18nModelForm): 'max_per_order', 'min_per_order', 'checkin_attention', + 'generate_tickets', 'original_price' ] field_classes = { @@ -349,6 +360,7 @@ class ItemUpdateForm(I18nModelForm): widgets = { 'available_from': SplitDateTimePickerWidget(), 'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}), + 'generate_tickets': TicketNullBooleanSelect() } diff --git a/src/pretix/control/templates/pretixcontrol/item/index.html b/src/pretix/control/templates/pretixcontrol/item/index.html index 0fbd42b08e..0af425fad4 100644 --- a/src/pretix/control/templates/pretixcontrol/item/index.html +++ b/src/pretix/control/templates/pretixcontrol/item/index.html @@ -43,6 +43,7 @@ {% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %} {% bootstrap_field form.require_approval layout="control" %} + {% bootstrap_field form.generate_tickets layout="control" %} {% for f in plugin_forms %} {% bootstrap_form f layout="control" %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index a7d87324ba..b09b028366 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -257,20 +257,18 @@ {% endif %} {% if not line.canceled %}
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 26e560739c..864bca541d 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -301,10 +301,8 @@ class OrderDownload(AsyncAction, OrderView): return self.error(_('You requested an invalid ticket output type.')) if not self.order_position: raise Http404(_('Unknown order code or not authorized to access this order.')) - if 'position' in kwargs and (self.order_position.addon_to and not self.request.event.settings.ticket_download_addons): - return self.error(_('Ticket download is not enabled for add-on products.')) - if 'position' in kwargs and (not self.order_position.item.admission and not self.request.event.settings.ticket_download_nonadm): - return self.error(_('Ticket download is not enabled for non-admission products.')) + if 'position' in kwargs and not self.order_position.generate_ticket: + return self.error(_('Ticket download is not enabled for this product.')) ct = self.get_last_ct() if ct: diff --git a/src/pretix/plugins/ticketoutputpdf/exporters.py b/src/pretix/plugins/ticketoutputpdf/exporters.py index aef6acdab1..ed7cb7e363 100644 --- a/src/pretix/plugins/ticketoutputpdf/exporters.py +++ b/src/pretix/plugins/ticketoutputpdf/exporters.py @@ -77,9 +77,7 @@ class AllTicketsPDF(BaseExporter): ) for op in qs: - if op.addon_to_id and not self.event.settings.ticket_download_addons: - continue - if not op.item.admission and not self.event.settings.ticket_download_nonadm: + if not op.generate_ticket: continue with language(op.order.locale): diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html index 4b8a9c2073..d495c1f944 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html @@ -56,20 +56,18 @@ {% endif %} - {% if download and line.item.admission|default:event.settings.ticket_download_nonadm %} + {% if download and line.generate_ticket %}