diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 37f538dfec..9c5112f617 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -41,6 +41,7 @@ payment_date date **DEPRECATED AN payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order total money (string) Total value of this order comment string Internal comment on this order +custom_followup_at date Internal date for a custom follow-up action checkin_attention boolean If ``true``, the check-in app should show a warning that this ticket requires special attention if a ticket of this order is scanned. @@ -123,6 +124,10 @@ last_modified datetime Last modificati The ``customer`` attribute has been added. +.. versionchanged:: 4.1 + + The ``custom_followup_at`` attribute has been added. + .. _order-position-resource: @@ -307,6 +312,7 @@ List of all orders "fees": [], "total": "23.00", "comment": "", + "custom_followup_at": null, "checkin_attention": false, "require_approval": false, "invoice_address": { @@ -478,6 +484,7 @@ Fetching individual orders "fees": [], "total": "23.00", "comment": "", + "custom_followup_at": null, "checkin_attention": false, "require_approval": false, "invoice_address": { @@ -644,6 +651,8 @@ Updating order fields * ``comment`` + * ``custom_followup_at`` + * ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address) **Example request**: @@ -817,6 +826,7 @@ Creating orders charge will be created), this is just informative in case you *handled the payment already*. * ``payment_date`` (optional) – Date and time of the completion of the payment. * ``comment`` (optional) + * ``custom_followup_at`` (optional) * ``checkin_attention`` (optional) * ``invoice_address`` (optional) diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index bad458092e..cc694a619e 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -654,7 +654,7 @@ class OrderSerializer(I18nAwareModelSerializer): model = Order fields = ( 'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date', - 'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads', + 'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads', 'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'url', 'customer' ) @@ -685,7 +685,7 @@ class OrderSerializer(I18nAwareModelSerializer): 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', 'phone'] + update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone'] if 'invoice_address' in validated_data: iadata = validated_data.pop('invoice_address') @@ -925,6 +925,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): min_length=5 ) comment = serializers.CharField(required=False, allow_blank=True) + custom_followup_at = serializers.DateField(required=False, allow_null=True) payment_provider = serializers.CharField(required=False, allow_null=True) payment_info = CompatibleJSONField(required=False) consume_carts = serializers.ListField(child=serializers.CharField(), required=False) @@ -943,7 +944,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): model = Order fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel', 'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', - 'force', 'send_email', 'simulate', 'customer') + 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at') def validate_payment_provider(self, pp): if pp is None: diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index f21bae3257..8e4a8aafcb 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -692,6 +692,16 @@ class OrderViewSet(viewsets.ModelViewSet): } ) + if 'custom_followup_at' in self.request.data and serializer.instance.custom_followup_at != self.request.data.get('custom_followup_at'): + serializer.instance.log_action( + 'pretix.event.order.custom_followup_at', + user=self.request.user, + auth=self.request.auth, + data={ + 'new_custom_followup_at': self.request.data.get('custom_followup_at') + } + ) + if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'): serializer.instance.log_action( 'pretix.event.order.checkin_attention', diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index 135e8888a4..85a1a4995c 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -288,6 +288,7 @@ class OrderListExporter(MultiSheetListExporter): headers.append(_('Sales channel')) headers.append(_('Requires special attention')) headers.append(_('Comment')) + headers.append(_('Follow-up date')) headers.append(_('Positions')) headers.append(_('E-mail address verified')) headers.append(_('Payment providers')) @@ -393,6 +394,7 @@ class OrderListExporter(MultiSheetListExporter): row.append(order.sales_channel) row.append(_('Yes') if order.checkin_attention else _('No')) row.append(order.comment or "") + row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "") row.append(order.pcnt) row.append(_('Yes') if order.email_known_to_work else _('No')) row.append(', '.join([ @@ -574,6 +576,7 @@ class OrderListExporter(MultiSheetListExporter): _('Seat row'), _('Seat number'), _('Order comment'), + _('Follow-up date'), ] questions = list(Question.objects.filter(event__in=self.events)) @@ -677,6 +680,7 @@ class OrderListExporter(MultiSheetListExporter): row += ['', '', '', '', ''] row.append(order.comment) + row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "") acache = {} for a in op.answers.all(): # We do not want to localize Date, Time and Datetime question answers, as those can lead @@ -780,7 +784,7 @@ class PaymentListExporter(ListExporter): headers = [ _('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'), - _('Status code'), _('Amount'), _('Payment method'), _('Comment') + _('Status code'), _('Amount'), _('Payment method'), _('Comment'), ] yield headers diff --git a/src/pretix/base/migrations/0193_auto_20210611_1355.py b/src/pretix/base/migrations/0193_auto_20210611_1355.py new file mode 100644 index 0000000000..65c249348b --- /dev/null +++ b/src/pretix/base/migrations/0193_auto_20210611_1355.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.3 on 2021-06-11 13:55 + +import django.db.models.manager +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0192_checkin_more_fields'), + ] + + operations = [ + migrations.AlterModelManagers( + name='checkin', + managers=[ + ('all', django.db.models.manager.Manager()), + ], + ), + migrations.AddField( + model_name='order', + name='custom_followup_at', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 71669d90c3..7e26c6fe06 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -60,7 +60,7 @@ from django.utils.crypto import get_random_string from django.utils.encoding import escape_uri_path from django.utils.formats import date_format from django.utils.functional import cached_property -from django.utils.timezone import make_aware, now +from django.utils.timezone import get_current_timezone, make_aware, now from django.utils.translation import gettext_lazy as _, pgettext_lazy from django_countries.fields import Country from django_scopes import ScopedManager, scopes_disabled @@ -217,6 +217,11 @@ class Order(LockModel, LoggedModel): help_text=_("The text entered in this field will not be visible to the user and is available for your " "convenience.") ) + custom_followup_at = models.DateField( + verbose_name=_("Follow-up date"), + help_text=_('We\'ll show you this order to be due for a follow-up on this day.'), + null=True, blank=True + ) checkin_attention = models.BooleanField( verbose_name=_('Requires special attention'), default=False, @@ -300,6 +305,10 @@ class Order(LockModel, LoggedModel): """ return self.all_fees(manager='objects') + @property + def custom_followup_due(self): + return self.custom_followup_at and self.custom_followup_at <= now().astimezone(get_current_timezone()).date() + @cached_property @scopes_disabled() def count_positions(self): diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 6a7415b331..cc3a9b4ceb 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -205,6 +205,10 @@ class OrderFilterForm(FilterForm): ('na', _('Approved, payment pending')), ('pa', _('Approval pending')), )), + (_('Follow-up date'), ( + ('custom_followup_at', _('Follow-up configured')), + ('custom_followup_due', _('Follow-up due')), + )), ('testmode', _('Test mode')), ), required=False, @@ -324,6 +328,14 @@ class OrderFilterForm(FilterForm): status=Order.STATUS_PENDING, require_approval=False ) + elif s == 'custom_followup_at': + qs = qs.filter( + custom_followup_at__isnull=False + ) + elif s == 'custom_followup_due': + qs = qs.filter( + custom_followup_at__lte=now().astimezone(get_current_timezone()).date() + ) elif s == 'testmode': qs = qs.filter( testmode=True diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 9d07dc070d..e21b4dcdc1 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -230,12 +230,13 @@ class ExporterForm(forms.Form): class CommentForm(I18nModelForm): class Meta: model = Order - fields = ['comment', 'checkin_attention'] + fields = ['comment', 'checkin_attention', 'custom_followup_at'] widgets = { 'comment': forms.Textarea(attrs={ 'rows': 3, 'class': 'helper-width-100', }), + 'custom_followup_at': DatePickerWidget(), } diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index b021b710c7..19e2c2f5b7 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -360,6 +360,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'), 'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'), 'pretix.event.order.comment': _('The order\'s internal comment has been updated.'), + 'pretix.event.order.custom_followup_at': _('The order\'s follow-up date has been updated.'), 'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been ' 'toggled.'), 'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'), diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 5ebaa0d421..a37eb767d2 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -890,10 +890,9 @@
{% csrf_token %} -
- {% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %} - {% bootstrap_field comment_form.checkin_attention layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %} -
+ {% bootstrap_field comment_form.comment show_help=True show_label=False %} + {% bootstrap_field comment_form.custom_followup_at %} + {% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %} diff --git a/src/pretix/control/templates/pretixcontrol/orders/index.html b/src/pretix/control/templates/pretixcontrol/orders/index.html index 22e05e69f4..0685f789dc 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/index.html +++ b/src/pretix/control/templates/pretixcontrol/orders/index.html @@ -134,6 +134,11 @@ {% if o.testmode %} {% trans "TEST MODE" %} {% endif %} + {% if o.custom_followup_due %} + {% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %} + {% elif o.custom_followup_at %} + {% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %} + {% endif %} {{ o.email|default_if_none:"" }} diff --git a/src/pretix/control/templates/pretixcontrol/search/orders.html b/src/pretix/control/templates/pretixcontrol/search/orders.html index d83c96138d..13bf6f9855 100644 --- a/src/pretix/control/templates/pretixcontrol/search/orders.html +++ b/src/pretix/control/templates/pretixcontrol/search/orders.html @@ -69,6 +69,11 @@ {% if o.testmode %} {% trans "TEST MODE" %} {% endif %} + {% if o.custom_followup_due %} + {% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %} + {% elif o.custom_followup_at %} + {% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %} + {% endif %} {{ o.event.name }} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index d2badeb18c..5a8f7fb20d 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -303,6 +303,7 @@ class OrderDetail(OrderView): ctx['invoices'] = list(self.order.invoices.all().select_related('event')) ctx['comment_form'] = CommentForm(initial={ 'comment': self.order.comment, + 'custom_followup_at': self.order.custom_followup_at, 'checkin_attention': self.order.checkin_attention }) ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale] @@ -487,12 +488,21 @@ class OrderComment(OrderView): 'new_comment': form.cleaned_data.get('comment') }) + if form.cleaned_data.get('custom_followup_at') != self.order.custom_followup_at: + self.order.custom_followup_at = form.cleaned_data.get('custom_followup_at') + self.order.log_action('pretix.event.order.custom_followup_at', user=self.request.user, data={ + 'new_custom_followup_at': form.cleaned_data.get('custom_followup_at') + }) + if form.cleaned_data.get('checkin_attention') != self.order.checkin_attention: self.order.checkin_attention = form.cleaned_data.get('checkin_attention') self.order.log_action('pretix.event.order.checkin_attention', user=self.request.user, data={ 'new_value': form.cleaned_data.get('checkin_attention') }) - self.order.save(update_fields=['checkin_attention', 'comment']) + print(self.order.custom_followup_at) + self.order.save(update_fields=['checkin_attention', 'comment', 'custom_followup_at']) + self.order.refresh_from_db() + print(self.order.custom_followup_at) messages.success(self.request, _('The comment has been updated.')) else: messages.error(self.request, _('Could not update the comment.')) diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 399d52d9be..07ee22f554 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -256,6 +256,7 @@ TEST_ORDER_RES = { "payment_provider": "banktransfer", "total": "23.00", "comment": "", + "custom_followup_at": None, "checkin_attention": False, "invoice_address": { "last_modified": "2017-12-01T10:00:00Z", @@ -1739,6 +1740,7 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques ], 'total': '23.25', 'comment': '', + "custom_followup_at": None, 'invoice_address': { 'is_business': False, 'company': 'Sample company', @@ -4202,6 +4204,7 @@ def test_order_update_allowed_fields(token_client, organizer, event, order): organizer.slug, event.slug, order.code ), format='json', data={ 'comment': 'Here is a comment', + 'custom_followup_at': '2021-06-12', 'checkin_attention': True, 'email': 'foo@bar.com', 'phone': '+4962219999', @@ -4224,6 +4227,7 @@ def test_order_update_allowed_fields(token_client, organizer, event, order): assert resp.status_code == 200 order.refresh_from_db() assert order.comment == 'Here is a comment' + assert order.custom_followup_at.isoformat() == '2021-06-12' assert order.checkin_attention assert order.email == 'foo@bar.com' assert order.phone == '+4962219999' @@ -4236,6 +4240,7 @@ def test_order_update_allowed_fields(token_client, organizer, event, order): assert order.invoice_address.city == "Paris" with scopes_disabled(): assert order.all_logentries().get(action_type='pretix.event.order.comment') + assert order.all_logentries().get(action_type='pretix.event.order.custom_followup_at') assert order.all_logentries().get(action_type='pretix.event.order.checkin_attention') assert order.all_logentries().get(action_type='pretix.event.order.contact.changed') assert order.all_logentries().get(action_type='pretix.event.order.phone.changed')