mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
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:
@@ -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):
|
||||
|
||||
26
src/pretix/base/migrations/0151_auto_20200421_0737.py
Normal file
26
src/pretix/base/migrations/0151_auto_20200421_0737.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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.'),
|
||||
}
|
||||
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
))
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user