Add expiry dates and individual conditions to gift cards (#1656)

* Add expiry dates and individual conditions to gift cards

* Display refund gift cards with more details and prettier interface

* Allow to set gift card expiry and conditions when cancelling event

* Extend gift card search

* Fix #1565 -- Some gift card filters

* Improve list of gift cards

* Allow to edit gift cards

* Note on validity
This commit is contained in:
Raphael Michel
2020-04-21 15:57:02 +02:00
committed by GitHub
parent d9fd4b33a0
commit f2844ac686
31 changed files with 450 additions and 70 deletions

View File

@@ -10,7 +10,7 @@ from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries import Countries
from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import (
@@ -577,6 +577,13 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_paid_require_approval',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.obj.settings.giftcard_expiry_years is not None:
self.fields['cancel_allow_user_paid_refund_as_giftcard'].help_text = gettext(
'You have configured gift cards to be valid {} years plus the year the gift card is issued in.'
).format(self.obj.settings.giftcard_expiry_years)
class PaymentSettingsForm(SettingsForm):
auto_fields = [

View File

@@ -78,7 +78,7 @@ class FilterForm(forms.Form):
def get_order_by(self):
o = self.cleaned_data.get('ordering')
if o.startswith('-'):
if o.startswith('-') and o not in self.orders:
return '-' + self.orders[o[1:]]
else:
return self.orders[o]
@@ -530,6 +530,33 @@ class OrganizerFilterForm(FilterForm):
class GiftCardFilterForm(FilterForm):
orders = {
'issuance': 'issuance',
'expires': F('expires').asc(nulls_last=True),
'-expires': F('expires').desc(nulls_first=True),
'secret': 'secret',
'value': 'cached_value',
}
testmode = forms.ChoiceField(
label=_('Test mode'),
choices=(
('', _('All')),
('yes', _('Test mode')),
('no', _('Live')),
),
required=False
)
state = forms.ChoiceField(
label=_('Empty'),
choices=(
('', _('All')),
('empty', _('Empty')),
('valid_value', _('Valid and with value')),
('expired_value', _('Expired and with value')),
('expired', _('Expired')),
),
required=False
)
query = forms.CharField(
label=_('Search query'),
widget=forms.TextInput(attrs={
@@ -548,8 +575,30 @@ class GiftCardFilterForm(FilterForm):
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(secret__icontains=query)
return qs
qs = qs.filter(
Q(secret__icontains=query)
| Q(transactions__text__icontains=query)
| Q(transactions__order__code__icontains=query)
)
if fdata.get('testmode') == 'yes':
qs = qs.filter(testmode=True)
elif fdata.get('testmode') == 'no':
qs = qs.filter(testmode=False)
if fdata.get('state') == 'empty':
qs = qs.filter(cached_value=0)
elif fdata.get('state') == 'valid_value':
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=now()))
elif fdata.get('state') == 'expired_value':
qs = qs.exclude(cached_value=0).filter(expires__lt=now())
elif fdata.get('state') == 'expired':
qs = qs.filter(expires__lt=now())
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by('-issuance')
return qs.distinct()
class EventFilterForm(FilterForm):

View File

@@ -15,12 +15,15 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget,
)
from pretix.base.models import (
InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2
from pretix.helpers.money import change_decimal_field
@@ -565,6 +568,20 @@ class EventCancelForm(forms.Form):
initial=False,
required=False,
)
gift_card_expires = SplitDateTimeField(
label=_('Gift card validity'),
required=False,
widget=SplitDateTimePickerWidget(
attrs={'data-display-dependency': '#id_refund_as_giftcard'},
)
)
gift_card_conditions = forms.CharField(
label=_('Special terms and conditions'),
required=False,
widget=forms.Textarea(
attrs={'rows': 2, 'data-display-dependency': '#id_refund_as_giftcard'},
)
)
keep_fee_fixed = forms.DecimalField(
label=_("Keep a fixed cancellation fee"),
max_digits=10, decimal_places=2,
@@ -615,6 +632,8 @@ class EventCancelForm(forms.Form):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
kwargs.setdefault('initial', {})
kwargs['initial']['gift_card_expires'] = self.event.organizer.default_gift_card_expiry
super().__init__(*args, **kwargs)
self.fields['send_subject'] = I18nFormField(
label=_("Subject"),

View File

@@ -14,9 +14,10 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Device, GiftCard, Organizer, Team
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget,
ExtFileField, FontSelect, MultipleLanguagesWidget, SplitDateTimeField,
)
from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain
@@ -330,6 +331,12 @@ class OrganizerSettingsForm(SettingsForm):
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
required=False
)
giftcard_expiry_years = forms.IntegerField(
label=_('Validity of gift card codes in years'),
help_text=_('If you set a number here, gift cards will by default expire at the end of the year after this '
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -378,12 +385,15 @@ class GiftCardCreateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
initial = kwargs.pop('initial', {})
initial['expires'] = self.organizer.default_gift_card_expiry
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
def clean_secret(self):
s = self.cleaned_data['secret']
if GiftCard.objects.filter(
secret__iexact=s
secret__iexact=s
).filter(
Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer)
).exists():
@@ -394,4 +404,24 @@ class GiftCardCreateForm(forms.ModelForm):
class Meta:
model = GiftCard
fields = ['secret', 'currency', 'testmode']
fields = ['secret', 'currency', 'testmode', 'expires', 'conditions']
field_classes = {
'expires': SplitDateTimeField
}
widgets = {
'expires': SplitDateTimePickerWidget,
'conditions': forms.Textarea(attrs={"rows": 2})
}
class GiftCardUpdateForm(forms.ModelForm):
class Meta:
model = GiftCard
fields = ['expires', 'conditions']
field_classes = {
'expires': SplitDateTimeField
}
widgets = {
'expires': SplitDateTimePickerWidget,
'conditions': forms.Textarea(attrs={"rows": 2})
}

View File

@@ -334,6 +334,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.device.keyroll': _('The access token of the device has been regenerated.'),
'pretix.device.updated': _('The device has notified the server of an hardware or software update.'),
'pretix.giftcards.created': _('The gift card has been created.'),
'pretix.giftcards.modified': _('The gift card has been changed.'),
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
}

View File

@@ -36,6 +36,8 @@
{% bootstrap_field form.auto_refund layout="control" %}
{% bootstrap_field form.manual_refund layout="control" %}
{% bootstrap_field form.refund_as_giftcard layout="control" %}
{% bootstrap_field form.gift_card_expires layout="control" %}
{% bootstrap_field form.gift_card_conditions layout="control" %}
{% bootstrap_field form.keep_fee_fixed layout="control" %}
{% bootstrap_field form.keep_fee_percentage layout="control" %}
{% bootstrap_field form.keep_fees layout="control" %}

View File

@@ -32,7 +32,6 @@
{% endif %}
{% bootstrap_field sform.organizer_info_text layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Organizer page" %}</legend>
@@ -63,6 +62,11 @@
{% bootstrap_field sform.primary_font layout="control" %}
{% bootstrap_field sform.favicon layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Gift cards" %}</legend>
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Event metadata" %}</legend>
<p>

View File

@@ -10,6 +10,10 @@
{% if card.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
<a href="{% url "control:organizer.giftcard.edit" organizer=request.organizer.slug giftcard=card.id %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
</h1>
<div class="row">
<div class="col-md-10 col-xs-12">
@@ -29,6 +33,12 @@
<dd>{{ card.value|money:card.currency }}</dd>
<dt>{% trans "Currency" %}</dt>
<dd>{{ card.currency }}</dd>
<dt>{% trans "Expire date" %}</dt>
<dd>{% if card.expires %}{{ card.expires|date:"SHORT_DATETIME_FORMAT" }}{% else %}{% endif %}</dd>
{% if card.conditions %}
<dt>{% trans "Special terms and conditions" context "giftcard" %}</dt>
<dd>{{ card.conditions }}</dd>
{% endif %}
{% if card.issued_in %}
<dt>{% trans "Issued through sale" %}</dt>
<dd>

View File

@@ -10,6 +10,8 @@
{% bootstrap_field form.value layout="control" %}
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.testmode layout="control" %}
{% bootstrap_field form.expires layout="control" %}
{% bootstrap_field form.conditions layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -0,0 +1,24 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>
{% blocktrans trimmed with card=card.secret %}
Gift card: {{ card }}
{% endblocktrans %}
{% if card.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</h1>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.expires layout="control" %}
{% bootstrap_field form.conditions layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load money %}
{% block inner %}
@@ -21,9 +22,15 @@
</div>
{% else %}
<form class="row filter-form" action="" method="get">
<div class="col-md-10 col-sm-6 col-xs-12">
<div class="col-md-4 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-6">
{% bootstrap_field filter_form.testmode layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-6">
{% bootstrap_field filter_form.state layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
@@ -41,9 +48,18 @@
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Gift card code" %}</th>
<th>{% trans "Creation date" %}</th>
<th class="text-right">{% trans "Current value" %}</th>
<th>{% trans "Gift card code" %}
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Creation date" %}
<a href="?{% url_replace request 'ordering' '-issuance' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'issuance' %}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Expiry date" %}
<a href="?{% url_replace request 'ordering' '-expires' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'expires' %}"><i class="fa fa-caret-up"></i></a></th>
<th class="text-right">{% trans "Current value" %}
<a href="?{% url_replace request 'ordering' '-value' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'value' %}"><i class="fa fa-caret-up"></i></a></th>
<th></th>
</tr>
</thead>
@@ -56,8 +72,12 @@
{% if g.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% if g.expired %}
<span class="label label-danger">{% trans "Expired" %}</span>
{% endif %}
</td>
<td>{{ g.issuance|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{% if g.expires %}{{ g.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
<td class="text-right">
{{ g.cached_value|money:g.currency }}
</td>

View File

@@ -79,6 +79,8 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/giftcards$', organizer.GiftCardListView.as_view(), name='organizer.giftcards'),
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/add$', organizer.GiftCardCreateView.as_view(), name='organizer.giftcard.add'),
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(),
name='organizer.giftcard.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
url(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
name='organizer.webhook.add'),

View File

@@ -770,6 +770,7 @@ class OrderRefundView(OrderView):
if giftcard_value:
refund_selected += giftcard_value
giftcard = self.request.organizer.issued_gift_cards.create(
expires=self.request.organizer.default_gift_card_expiry,
currency=self.request.event.currency,
testmode=self.order.testmode
)
@@ -2019,6 +2020,8 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
auto_refund=form.cleaned_data.get('auto_refund'),
manual_refund=form.cleaned_data.get('manual_refund'),
refund_as_giftcard=form.cleaned_data.get('refund_as_giftcard'),
giftcard_expires=form.cleaned_data.get('gift_card_expires'),
giftcard_conditions=form.cleaned_data.get('gift_card_conditions'),
keep_fee_fixed=form.cleaned_data.get('keep_fee_fixed'),
keep_fee_percentage=form.cleaned_data.get('keep_fee_percentage'),
keep_fees=form.cleaned_data.get('keep_fees'),

View File

@@ -34,9 +34,9 @@ from pretix.control.forms.filter import (
EventFilterForm, GiftCardFilterForm, OrganizerFilterForm,
)
from pretix.control.forms.organizer import (
DeviceForm, EventMetaPropertyForm, GiftCardCreateForm, OrganizerDeleteForm,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
WebHookForm,
DeviceForm, EventMetaPropertyForm, GiftCardCreateForm, GiftCardUpdateForm,
OrganizerDeleteForm, OrganizerForm, OrganizerSettingsForm,
OrganizerUpdateForm, TeamForm, WebHookForm,
)
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
@@ -1099,3 +1099,31 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
'giftcard': self.object.pk
}
))
class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/giftcard_edit.html'
permission = 'can_manage_gift_cards'
form_class = GiftCardUpdateForm
success_url = 'invalid'
context_object_name = 'card'
model = GiftCard
def get_object(self, queryset=None) -> Organizer:
return get_object_or_404(
self.request.organizer.issued_gift_cards,
pk=self.kwargs.get('giftcard')
)
@transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _('The gift card has been changed.'))
super().form_valid(form)
form.instance.log_action('pretix.giftcards.modified', user=self.request.user, data=dict(form.cleaned_data))
return redirect(reverse(
'control:organizer.giftcard',
kwargs={
'organizer': self.request.organizer.slug,
'giftcard': self.object.pk
}
))