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

* Add expiry dates and individual conditions to gift cards

* Display refund gift cards with more details and prettier interface

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

* Extend gift card search

* Fix #1565 -- Some gift card filters

* Improve list of gift cards

* Allow to edit gift cards

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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