forked from CGM_Public/pretix_original
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:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
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 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')
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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" %}
|
{% 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>
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user