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

@@ -55,7 +55,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
class EventSlugField(serializers.SlugRelatedField):

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.0.5 on 2020-04-21 07:37
import django_countries.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0150_auto_20200401_1123'),
]
operations = [
migrations.AddField(
model_name='giftcard',
name='conditions',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='giftcard',
name='expires',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -5,7 +5,8 @@ from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Sum
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.banlist import banned
from pretix.base.models import LoggedModel
@@ -62,12 +63,22 @@ class GiftCard(LoggedModel):
verbose_name=_('Test mode card'),
default=False
)
expires = models.DateTimeField(
null=True, blank=True, verbose_name=_('Expiry date')
)
conditions = models.TextField(
null=True, blank=True, verbose_name=pgettext_lazy('giftcard', 'Special terms and conditions')
)
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
currency = models.CharField(max_length=10, choices=CURRENCY_CHOICES)
def __str__(self):
return self.secret
@property
def expired(self):
return self.expires and now() > self.expires
@property
def value(self):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')

View File

@@ -1,10 +1,12 @@
import string
from datetime import date, datetime, time
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext_lazy as _
from pretix.base.models.base import LoggedModel
@@ -101,6 +103,15 @@ class Organizer(LoggedModel):
Q(issuer=self) | Q(accepted=True)
)
@property
def default_gift_card_expiry(self):
if self.settings.giftcard_expiry_years is not None:
tz = get_current_timezone()
return make_aware(datetime.combine(
date(now().astimezone(tz).year + self.settings.get('giftcard_expiry_years', as_type=int), 12, 31),
time(hour=23, minute=59, second=59)
), tz)
def allow_delete(self):
from . import Order, Invoice
return (

View File

@@ -1097,6 +1097,9 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.expires and gc.expires < now():
messages.error(request, _("This gift card is no longer valid."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
@@ -1156,6 +1159,9 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.expires and gc.expires < now():
messages.error(request, _("This gift card is no longer valid."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
@@ -1194,6 +1200,9 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if payment.amount > gc.value: # noqa - just a safeguard
raise PaymentException(_("This gift card was used in the meantime. Please try again"))
if gc.expires and gc.expires < now(): # noqa - just a safeguard
messages.error(request, _("This gift card is no longer valid."))
return
trans = gc.transactions.create(
value=-1 * payment.amount,
order=payment.order,

View File

@@ -86,7 +86,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
send: bool=False, send_subject: dict=None, send_message: dict=None,
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
user: int=None, refund_as_giftcard: bool=False):
user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None):
send_subject = LazyI18nString(send_subject)
send_message = LazyI18nString(send_message)
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
@@ -169,7 +169,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
try:
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard)
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions)
finally:
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
@@ -213,7 +214,9 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
refund_amount = o.payment_refund_sum - o.total
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)

View File

@@ -1913,8 +1913,11 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
raise OrderError(str(error_messages['busy']))
_unset = object()
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
refund_as_giftcard=False):
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None):
notify_admin = False
error = False
if isinstance(order, int):
@@ -1929,6 +1932,8 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
can_auto_refund_sum = refund_amount
with transaction.atomic():
giftcard = order.event.organizer.issued_gift_cards.create(
expires=order.event.organizer.default_gift_card_expiry if giftcard_expires is _unset else giftcard_expires,
conditions=giftcard_conditions,
currency=order.event.currency,
testmode=order.testmode
)
@@ -2144,7 +2149,8 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
issued += gc.transactions.first().value
if p.price - issued > 0:
gc = sender.organizer.issued_gift_cards.create(
currency=sender.currency, issued_in=p, testmode=order.testmode
currency=sender.currency, issued_in=p, testmode=order.testmode,
expires=sender.organizer.default_gift_card_expiry,
)
gc.transactions.create(value=p.price - issued, order=order)
any_giftcards = True

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
}
))

View File

@@ -103,27 +103,62 @@
</div>
{% endif %}
{% if refunds %}
<div class="alert alert-info">
{% for r in refunds %}
{% if r.state == "created" or r.state == "transit" %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
A refund of {{ amount }} will be sent out to you soon, please be patient.
{% endblocktrans %}
{% elif r.state == "done" %}
{% if r.provider == "giftcard" and "gift_card_code" in r.info_data %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency code=r.info_data.gift_card_code %}
We've issued your refund of {{ amount }} as a gift card. On your next purchase with
us, you can use the gift card code <strong>{{ code }}</strong> during payment.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
A refund of {{ amount }} has been sent to you. Depending on the payment method, please allow for up to 14 days until it shows up
on your statement.
{% endblocktrans %}
{% endif %}
{% endif %}
{% if not forloop.last %}<br />{% endif %}
{% endfor %}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Refunds" %}
</h3>
</div>
<ul class="list-group">
{% for r in refunds %}
<li class="list-group-item">
{% if r.state == "created" or r.state == "transit" %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
A refund of {{ amount }} will be sent out to you soon, please be patient.
{% endblocktrans %}
{% elif r.state == "done" %}
{% if r.provider == "giftcard" %}
<a href="#" class="print-this-page btn btn-default btn-sm pull-right hidden-print">
<span class="fa fa-print"></span>
{% trans "Print" %}
</a>
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
We've issued your refund of {{ amount }} as a gift card. On your next purchase with
us, you can use the following gift card code during payment:
{% endblocktrans %}
<div class="text-center refund-gift-card-code">
<span class="fa fa-credit-card"></span>
{{ r.giftcard.secret }}
</div>
{% if r.giftcard.value != r.amount %}
<small>
{% blocktrans trimmed with value=r.giftcard.value|money:request.event.currency %}
The current value of your gift card is {{ value }}.
{% endblocktrans %}
</small>
{% endif %}
{% if r.giftcard.expires %}
<small>
{% blocktrans trimmed with expiry=r.giftcard.expires|date:"SHORT_DATE_FORMAT" %}
This gift card is valid until {{ expiry }}.
{% endblocktrans %}
</small>
{% endif %}
{% if r.giftcard.conditions %}
<small>
{{ r.giftcard.conditions }}
</small>
{% endif %}
{% else %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
A refund of {{ amount }} has been sent to you. Depending on the payment method, please allow for up to 14 days until it shows up
on your statement.
{% endblocktrans %}
{% endif %}
{% endif %}
{% endfor %}
</li>
</ul>
</div>
{% endif %}
{% endif %}

View File

@@ -81,36 +81,54 @@
<strong>
{% trans "The refund will be issued in form of a gift card that you can use for further purchases." %}
</strong>
{% if request.event.organizer.default_gift_card_expiry %}
<p class="help-block">
{% blocktrans trimmed with expiry_date=request.event.organizer.default_gift_card_expiry|date:"SHORT_DATE_FORMAT" %}
Your gift card will be valid until {{ expiry_date }}.
{% endblocktrans %}
</p>
{% endif %}
{% elif request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "option" %}
<div class="radio">
<label>
<input type="radio" name="giftcard" value="true" checked>
<input type="radio" name="giftcard" value="true" checked id="id_giftcard">
<strong>{% trans "I want the refund as a gift card for later purchases" %}</strong>
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="giftcard" value="false" checked>
<input type="radio" name="giftcard" value="false" checked id="id_payout">
<strong>{% trans "I want the refund to be sent to my original payment method" %}</strong>
</label>
</div>
{% if can_auto_refund %}
<p class="help-block">
{% blocktrans trimmed %}
The refund amount will automatically be sent back to your original payment method. Depending
on the payment method, please allow for up to two weeks before this appears on your
statement.
{% endblocktrans %}
</p>
{% else %}
<p class="help-block">
{% blocktrans trimmed %}
With the payment method you used, the refund amount <strong>can not be sent back to you
automatically</strong>. Instead, the event organizer will need to initiate the transfer
manually. Please be patient as this might take a bit longer.
{% endblocktrans %}
</p>
{% endif %}
<div data-display-dependency="#id_giftcard">
{% if request.event.organizer.default_gift_card_expiry %}
<p class="help-block">
{% blocktrans trimmed with expiry_date=request.event.organizer.default_gift_card_expiry|date:"SHORT_DATE_FORMAT" %}
Your gift card will be valid until {{ expiry_date }}.
{% endblocktrans %}
</p>
{% endif %}
</div>
<div data-display-dependency="#id_payout">
{% if can_auto_refund %}
<p class="help-block">
{% blocktrans trimmed %}
The refund amount will automatically be sent back to your original payment method. Depending
on the payment method, please allow for up to two weeks before this appears on your
statement.
{% endblocktrans %}
</p>
{% else %}
<p class="help-block">
{% blocktrans trimmed %}
With the payment method you used, the refund amount <strong>can not be sent back to you
automatically</strong>. Instead, the event organizer will need to initiate the transfer
manually. Please be patient as this might take a bit longer.
{% endblocktrans %}
</p>
{% endif %}
</div>
{% else %}
{% if can_auto_refund %}
<p>

View File

@@ -20,7 +20,7 @@ from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import TemplateView, View
from pretix.base.models import (
CachedTicket, Invoice, Order, OrderPosition, Quota,
CachedTicket, GiftCard, Invoice, Order, OrderPosition, Quota,
)
from pretix.base.models.orders import (
CachedCombinedTicket, OrderFee, OrderPayment, OrderRefund, QuestionAnswer,
@@ -230,6 +230,10 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
).exclude(
provider__in=('offsetting', 'reseller', 'boxoffice', 'manual')
)
for r in ctx['refunds']:
if r.provider == 'giftcard':
gc = GiftCard.objects.get(pk=r.info_data.get('gift_card'))
r.giftcard = gc
return ctx

View File

@@ -247,6 +247,12 @@ $(function () {
$tr.show();
});
$(".print-this-page").on("click", function (e) {
window.print();
e.preventDefault();
return true;
});
// Invoice address form
$("input[data-required-if]").each(function () {
var dependent = $(this),
@@ -263,24 +269,28 @@ $(function () {
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
$("input[data-display-dependency]").each(function () {
$("input[data-display-dependency], div[data-display-dependency]").each(function () {
var dependent = $(this),
dependency = $($(this).attr("data-display-dependency")),
update = function (ev) {
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
var $toggling = dependent;
if (dependent.get(0).tagName.toLowerCase() !== "div") {
$toggling = dependent.closest('.form-group');
}
if (ev) {
if (enabled) {
dependent.closest('.form-group').stop().slideDown();
$toggling.stop().slideDown();
} else {
dependent.closest('.form-group').stop().slideUp();
$toggling.stop().slideUp();
}
} else {
dependent.closest('.form-group').toggle(enabled);
$toggling.stop().toggle(enabled);
}
};
update();
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
$("select[name$=state]").each(function () {

View File

@@ -230,3 +230,8 @@ h2.subevent-head {
.info-download {
margin-top: 15px;
}
.refund-gift-card-code {
font-size: 24px;
font-family: $font-family-monospace;
padding: 8px 0;
}