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

@@ -18,6 +18,8 @@ secret string Gift card code
value money (string) Current gift card value value money (string) Current gift card value
currency string Currency of the value (can not be modified later) currency string Currency of the value (can not be modified later)
testmode boolean Whether this is a test gift card testmode boolean Whether this is a test gift card
expires datetime Expiry date (or ``null``)
conditions string Special terms and conditions for this card (or ``null``)
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
Endpoints Endpoints
@@ -53,6 +55,8 @@ Endpoints
"secret": "HLBYVELFRC77NCQY", "secret": "HLBYVELFRC77NCQY",
"currency": "EUR", "currency": "EUR",
"testmode": false, "testmode": false,
"expires": null,
"conditions": null,
"value": "13.37" "value": "13.37"
} }
] ]
@@ -92,6 +96,8 @@ Endpoints
"secret": "HLBYVELFRC77NCQY", "secret": "HLBYVELFRC77NCQY",
"currency": "EUR", "currency": "EUR",
"testmode": false, "testmode": false,
"expires": null,
"conditions": null,
"value": "13.37" "value": "13.37"
} }
@@ -134,6 +140,8 @@ Endpoints
"secret": "HLBYVELFRC77NCQY", "secret": "HLBYVELFRC77NCQY",
"testmode": false, "testmode": false,
"currency": "EUR", "currency": "EUR",
"expires": null,
"conditions": null,
"value": "13.37" "value": "13.37"
} }
@@ -180,6 +188,8 @@ Endpoints
"secret": "HLBYVELFRC77NCQY", "secret": "HLBYVELFRC77NCQY",
"testmode": false, "testmode": false,
"currency": "EUR", "currency": "EUR",
"expires": null,
"conditions": null,
"value": "14.00" "value": "14.00"
} }
@@ -222,6 +232,8 @@ Endpoints
"secret": "HLBYVELFRC77NCQY", "secret": "HLBYVELFRC77NCQY",
"currency": "EUR", "currency": "EUR",
"testmode": false, "testmode": false,
"expires": null,
"conditions": null,
"value": "15.37" "value": "15.37"
} }

View File

@@ -55,7 +55,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = GiftCard model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode') fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
class EventSlugField(serializers.SlugRelatedField): 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 import models
from django.db.models import Sum from django.db.models import Sum
from django.utils.crypto import get_random_string 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.banlist import banned
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
@@ -62,12 +63,22 @@ class GiftCard(LoggedModel):
verbose_name=_('Test mode card'), verbose_name=_('Test mode card'),
default=False 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_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
currency = models.CharField(max_length=10, choices=CURRENCY_CHOICES) currency = models.CharField(max_length=10, choices=CURRENCY_CHOICES)
def __str__(self): def __str__(self):
return self.secret return self.secret
@property
def expired(self):
return self.expires and now() > self.expires
@property @property
def value(self): def value(self):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00') return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')

View File

@@ -1,10 +1,12 @@
import string import string
from datetime import date, datetime, time
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property 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 django.utils.translation import gettext_lazy as _
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
@@ -101,6 +103,15 @@ class Organizer(LoggedModel):
Q(issuer=self) | Q(accepted=True) 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): def allow_delete(self):
from . import Order, Invoice from . import Order, Invoice
return ( return (

View File

@@ -1097,6 +1097,9 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and self.event.testmode: if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode.")) messages.error(request, _("Only test gift cards can be used in test mode."))
return 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"): if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used.")) messages.error(request, _("All credit on this gift card has been used."))
return return
@@ -1156,6 +1159,9 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and payment.order.testmode: if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode.")) messages.error(request, _("Only test gift cards can be used in test mode."))
return 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"): if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used.")) messages.error(request, _("All credit on this gift card has been used."))
return return
@@ -1194,6 +1200,9 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card is not accepted by this event organizer.")) raise PaymentException(_("This gift card is not accepted by this event organizer."))
if payment.amount > gc.value: # noqa - just a safeguard if payment.amount > gc.value: # noqa - just a safeguard
raise PaymentException(_("This gift card was used in the meantime. Please try again")) 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( trans = gc.transactions.create(
value=-1 * payment.amount, value=-1 * payment.amount,
order=payment.order, 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, keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
send: bool=False, send_subject: dict=None, send_message: dict=None, send: bool=False, send_subject: dict=None, send_message: dict=None,
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={}, 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_subject = LazyI18nString(send_subject)
send_message = LazyI18nString(send_message) send_message = LazyI18nString(send_message)
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject) 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: try:
if auto_refund: if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, _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: finally:
if send: if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all()) _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 refund_amount = o.payment_refund_sum - o.total
if auto_refund: 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: if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions) _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'])) raise OrderError(str(error_messages['busy']))
_unset = object()
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER, 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 notify_admin = False
error = False error = False
if isinstance(order, int): 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 can_auto_refund_sum = refund_amount
with transaction.atomic(): with transaction.atomic():
giftcard = order.event.organizer.issued_gift_cards.create( 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, currency=order.event.currency,
testmode=order.testmode testmode=order.testmode
) )
@@ -2144,7 +2149,8 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
issued += gc.transactions.first().value issued += gc.transactions.first().value
if p.price - issued > 0: if p.price - issued > 0:
gc = sender.organizer.issued_gift_cards.create( 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) gc.transactions.create(value=p.price - issued, order=order)
any_giftcards = True any_giftcards = True

View File

@@ -10,7 +10,7 @@ from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name 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 import Countries
from django_countries.fields import LazyTypedChoiceField from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import ( from i18nfield.forms import (
@@ -577,6 +577,13 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_paid_require_approval', '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): class PaymentSettingsForm(SettingsForm):
auto_fields = [ auto_fields = [

View File

@@ -78,7 +78,7 @@ class FilterForm(forms.Form):
def get_order_by(self): def get_order_by(self):
o = self.cleaned_data.get('ordering') o = self.cleaned_data.get('ordering')
if o.startswith('-'): if o.startswith('-') and o not in self.orders:
return '-' + self.orders[o[1:]] return '-' + self.orders[o[1:]]
else: else:
return self.orders[o] return self.orders[o]
@@ -530,6 +530,33 @@ class OrganizerFilterForm(FilterForm):
class GiftCardFilterForm(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( query = forms.CharField(
label=_('Search query'), label=_('Search query'),
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
@@ -548,8 +575,30 @@ class GiftCardFilterForm(FilterForm):
if fdata.get('query'): if fdata.get('query'):
query = fdata.get('query') query = fdata.get('query')
qs = qs.filter(secret__icontains=query) qs = qs.filter(
return qs 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): 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.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator 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 ( from pretix.base.models import (
InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition, InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition,
) )
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.helpers.money import change_decimal_field from pretix.helpers.money import change_decimal_field
@@ -565,6 +568,20 @@ class EventCancelForm(forms.Form):
initial=False, initial=False,
required=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( keep_fee_fixed = forms.DecimalField(
label=_("Keep a fixed cancellation fee"), label=_("Keep a fixed cancellation fee"),
max_digits=10, decimal_places=2, max_digits=10, decimal_places=2,
@@ -615,6 +632,8 @@ class EventCancelForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event') self.event = kwargs.pop('event')
kwargs.setdefault('initial', {})
kwargs['initial']['gift_card_expires'] = self.event.organizer.default_gift_card_expiry
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['send_subject'] = I18nFormField( self.fields['send_subject'] = I18nFormField(
label=_("Subject"), label=_("Subject"),

View File

@@ -14,9 +14,10 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.api.models import WebHook from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm 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.base.models import Device, GiftCard, Organizer, Team
from pretix.control.forms import ( from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, ExtFileField, FontSelect, MultipleLanguagesWidget, SplitDateTimeField,
) )
from pretix.control.forms.event import SafeEventMultipleChoiceField from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain 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'])), 'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
required=False 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -378,12 +385,15 @@ class GiftCardCreateForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer') self.organizer = kwargs.pop('organizer')
initial = kwargs.pop('initial', {})
initial['expires'] = self.organizer.default_gift_card_expiry
kwargs['initial'] = initial
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def clean_secret(self): def clean_secret(self):
s = self.cleaned_data['secret'] s = self.cleaned_data['secret']
if GiftCard.objects.filter( if GiftCard.objects.filter(
secret__iexact=s secret__iexact=s
).filter( ).filter(
Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer) Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer)
).exists(): ).exists():
@@ -394,4 +404,24 @@ class GiftCardCreateForm(forms.ModelForm):
class Meta: class Meta:
model = GiftCard 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.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.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.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.'), '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.auto_refund layout="control" %}
{% bootstrap_field form.manual_refund layout="control" %} {% bootstrap_field form.manual_refund layout="control" %}
{% bootstrap_field form.refund_as_giftcard 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_fixed layout="control" %}
{% bootstrap_field form.keep_fee_percentage layout="control" %} {% bootstrap_field form.keep_fee_percentage layout="control" %}
{% bootstrap_field form.keep_fees layout="control" %} {% bootstrap_field form.keep_fees layout="control" %}

View File

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

View File

@@ -10,6 +10,10 @@
{% if card.testmode %} {% if card.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span> <span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %} {% 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> </h1>
<div class="row"> <div class="row">
<div class="col-md-10 col-xs-12"> <div class="col-md-10 col-xs-12">
@@ -29,6 +33,12 @@
<dd>{{ card.value|money:card.currency }}</dd> <dd>{{ card.value|money:card.currency }}</dd>
<dt>{% trans "Currency" %}</dt> <dt>{% trans "Currency" %}</dt>
<dd>{{ card.currency }}</dd> <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 %} {% if card.issued_in %}
<dt>{% trans "Issued through sale" %}</dt> <dt>{% trans "Issued through sale" %}</dt>
<dd> <dd>

View File

@@ -10,6 +10,8 @@
{% bootstrap_field form.value layout="control" %} {% bootstrap_field form.value layout="control" %}
{% bootstrap_field form.currency layout="control" %} {% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.testmode 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"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "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" %} {% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %} {% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load money %} {% load money %}
{% block inner %} {% block inner %}
@@ -21,9 +22,15 @@
</div> </div>
{% else %} {% else %}
<form class="row filter-form" action="" method="get"> <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' %} {% bootstrap_field filter_form.query layout='inline' %}
</div> </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"> <div class="col-md-2 col-sm-6 col-xs-12">
<button class="btn btn-primary btn-block" type="submit"> <button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span> <span class="fa fa-filter"></span>
@@ -41,9 +48,18 @@
<table class="table table-condensed table-hover"> <table class="table table-condensed table-hover">
<thead> <thead>
<tr> <tr>
<th>{% trans "Gift card code" %}</th> <th>{% trans "Gift card code" %}
<th>{% trans "Creation date" %}</th> <a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
<th class="text-right">{% trans "Current value" %}</th> <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> <th></th>
</tr> </tr>
</thead> </thead>
@@ -56,8 +72,12 @@
{% if g.testmode %} {% if g.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span> <span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %} {% endif %}
{% if g.expired %}
<span class="label label-danger">{% trans "Expired" %}</span>
{% endif %}
</td> </td>
<td>{{ g.issuance|date:"SHORT_DATETIME_FORMAT" }}</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"> <td class="text-right">
{{ g.cached_value|money:g.currency }} {{ g.cached_value|money:g.currency }}
</td> </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>[^/]+)/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/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>[^/]+)/$', 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>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
url(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(), url(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
name='organizer.webhook.add'), name='organizer.webhook.add'),

View File

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

View File

@@ -34,9 +34,9 @@ from pretix.control.forms.filter import (
EventFilterForm, GiftCardFilterForm, OrganizerFilterForm, EventFilterForm, GiftCardFilterForm, OrganizerFilterForm,
) )
from pretix.control.forms.organizer import ( from pretix.control.forms.organizer import (
DeviceForm, EventMetaPropertyForm, GiftCardCreateForm, OrganizerDeleteForm, DeviceForm, EventMetaPropertyForm, GiftCardCreateForm, GiftCardUpdateForm,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, OrganizerDeleteForm, OrganizerForm, OrganizerSettingsForm,
WebHookForm, OrganizerUpdateForm, TeamForm, WebHookForm,
) )
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin, AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
@@ -1099,3 +1099,31 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
'giftcard': self.object.pk '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> </div>
{% endif %} {% endif %}
{% if refunds %} {% if refunds %}
<div class="alert alert-info"> <div class="panel panel-primary">
{% for r in refunds %} <div class="panel-heading">
{% if r.state == "created" or r.state == "transit" %} <h3 class="panel-title">
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %} {% trans "Refunds" %}
A refund of {{ amount }} will be sent out to you soon, please be patient. </h3>
{% endblocktrans %} </div>
{% elif r.state == "done" %} <ul class="list-group">
{% if r.provider == "giftcard" and "gift_card_code" in r.info_data %} {% for r in refunds %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency code=r.info_data.gift_card_code %} <li class="list-group-item">
We've issued your refund of {{ amount }} as a gift card. On your next purchase with {% if r.state == "created" or r.state == "transit" %}
us, you can use the gift card code <strong>{{ code }}</strong> during payment. {% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
{% endblocktrans %} A refund of {{ amount }} will be sent out to you soon, please be patient.
{% else %} {% endblocktrans %}
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %} {% elif r.state == "done" %}
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 {% if r.provider == "giftcard" %}
on your statement. <a href="#" class="print-this-page btn btn-default btn-sm pull-right hidden-print">
{% endblocktrans %} <span class="fa fa-print"></span>
{% endif %} {% trans "Print" %}
{% endif %} </a>
{% if not forloop.last %}<br />{% endif %} {% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
{% endfor %} 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> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

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

View File

@@ -20,7 +20,7 @@ from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.models import ( from pretix.base.models import (
CachedTicket, Invoice, Order, OrderPosition, Quota, CachedTicket, GiftCard, Invoice, Order, OrderPosition, Quota,
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
CachedCombinedTicket, OrderFee, OrderPayment, OrderRefund, QuestionAnswer, CachedCombinedTicket, OrderFee, OrderPayment, OrderRefund, QuestionAnswer,
@@ -230,6 +230,10 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
).exclude( ).exclude(
provider__in=('offsetting', 'reseller', 'boxoffice', 'manual') 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 return ctx

View File

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

View File

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

View File

@@ -27,6 +27,8 @@ TEST_GC_RES = {
"secret": "ABCDEF", "secret": "ABCDEF",
"value": "23.00", "value": "23.00",
"testmode": False, "testmode": False,
"expires": None,
"conditions": None,
"currency": "EUR" "currency": "EUR"
} }

View File

@@ -89,6 +89,16 @@ def test_card_detail_view_transact(organizer, admin_user, gift_card, client):
assert gift_card.all_logentries().count() == 1 assert gift_card.all_logentries().count() == 1
@pytest.mark.django_db
def test_card_detail_edit(organizer, admin_user, gift_card, client):
client.login(email='dummy@dummy.dummy', password='dummy')
client.post('/control/organizer/dummy/giftcard/{}/edit'.format(gift_card.pk), {
'conditions': 'Foo'
})
gift_card.refresh_from_db()
assert gift_card.conditions == 'Foo'
@pytest.mark.django_db @pytest.mark.django_db
def test_card_detail_view_transact_revert_refund(organizer, admin_user, gift_card, client): def test_card_detail_view_transact_revert_refund(organizer, admin_user, gift_card, client):
with scopes_disabled(): with scopes_disabled():

View File

@@ -150,6 +150,7 @@ organizer_urls = [
'organizer/abc/giftcards', 'organizer/abc/giftcards',
'organizer/abc/giftcard/add', 'organizer/abc/giftcard/add',
'organizer/abc/giftcard/1/', 'organizer/abc/giftcard/1/',
'organizer/abc/giftcard/1/edit',
] ]
@@ -413,6 +414,7 @@ organizer_permission_urls = [
("can_manage_gift_cards", "organizer/dummy/giftcards", 200), ("can_manage_gift_cards", "organizer/dummy/giftcards", 200),
("can_manage_gift_cards", "organizer/dummy/giftcard/add", 200), ("can_manage_gift_cards", "organizer/dummy/giftcard/add", 200),
("can_manage_gift_cards", "organizer/dummy/giftcard/1/", 404), ("can_manage_gift_cards", "organizer/dummy/giftcard/1/", 404),
("can_manage_gift_cards", "organizer/dummy/giftcard/1/edit", 404),
] ]

View File

@@ -1027,6 +1027,21 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
assert o.payments.get(provider='giftcard').amount == Decimal('18.00') assert o.payments.get(provider='giftcard').amount == Decimal('18.00')
assert o.payments.get(provider='banktransfer').amount == Decimal('5.00') assert o.payments.get(provider='banktransfer').amount == Decimal('5.00')
def test_giftcard_expired(self):
gc = self.orga.issued_gift_cards.create(currency="EUR", expires=now() - timedelta(days=1))
gc.transactions.create(value=20)
self.event.settings.set('payment_banktransfer__enabled', True)
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'giftcard',
'giftcard': gc.secret
}, follow=True)
assert 'This gift card is no longer valid.' in response.rendered_content
def test_giftcard_invalid_currency(self): def test_giftcard_invalid_currency(self):
gc = self.orga.issued_gift_cards.create(currency="USD") gc = self.orga.issued_gift_cards.create(currency="USD")
gc.transactions.create(value=20) gc.transactions.create(value=20)