mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Backend management of gift cards
This commit is contained in:
@@ -502,6 +502,29 @@ class OrganizerFilterForm(FilterForm):
|
||||
return qs
|
||||
|
||||
|
||||
class GiftCardFilterForm(FilterForm):
|
||||
query = forms.CharField(
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('query'):
|
||||
query = fdata.get('query')
|
||||
qs = qs.filter(secret__icontains=query)
|
||||
return qs
|
||||
|
||||
|
||||
class EventFilterForm(FilterForm):
|
||||
orders = {
|
||||
'slug': 'slug',
|
||||
|
||||
@@ -4,6 +4,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
@@ -12,7 +13,7 @@ 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.models import Device, Organizer, Team
|
||||
from pretix.base.models import Device, GiftCard, Organizer, Team
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, FontSelect, MultipleLanguagesWidget,
|
||||
)
|
||||
@@ -145,6 +146,7 @@ class TeamForm(forms.ModelForm):
|
||||
model = Team
|
||||
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
|
||||
'can_change_teams', 'can_change_organizer_settings',
|
||||
'can_manage_gift_cards',
|
||||
'can_change_event_settings', 'can_change_items',
|
||||
'can_view_orders', 'can_change_orders',
|
||||
'can_view_vouchers', 'can_change_vouchers']
|
||||
@@ -328,3 +330,29 @@ class WebHookForm(forms.ModelForm):
|
||||
field_classes = {
|
||||
'limit_events': SafeModelMultipleChoiceField
|
||||
}
|
||||
|
||||
|
||||
class GiftCardCreateForm(forms.ModelForm):
|
||||
value = forms.DecimalField(
|
||||
label=_('Gift card value')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_secret(self):
|
||||
s = self.cleaned_data['secret']
|
||||
if GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer)
|
||||
).exists():
|
||||
raise ValidationError(
|
||||
_('A gift card with the same secret already exists in your or an affiliated organizer account.')
|
||||
)
|
||||
return s
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['secret', 'currency']
|
||||
|
||||
@@ -437,6 +437,16 @@ def get_organizer_navigation(request):
|
||||
'active': 'organizer.device' in url.url_name,
|
||||
'icon': 'tablet',
|
||||
})
|
||||
if 'can_manage_gift_cards' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Gift cards'),
|
||||
'url': reverse('control:organizer.giftcards', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcard' in url.url_name,
|
||||
'icon': 'credit-card',
|
||||
})
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Webhooks'),
|
||||
'url': reverse('control:organizer.webhooks', kwargs={
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with card=card.secret %}
|
||||
Gift card: {{ card }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Details" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Gift card code" %}</dt>
|
||||
<dd>{{ card.secret }}</dd>
|
||||
<dt>{% trans "Creation date" %}</dt>
|
||||
<dd>{{ card.issuance|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Current value" %}</dt>
|
||||
<dd>{{ card.value|money:card.currency }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ card.currency }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Transactions" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Order" %}</th>
|
||||
<th class="text-right">{% trans "Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in card.transactions.all %}
|
||||
<tr>
|
||||
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{% if t.order %}
|
||||
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
|
||||
{{ t.order.full_code }}
|
||||
{% else %}
|
||||
<em>{% trans "Manual transaction" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ t.value|money:card.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
<form class="helper-display-inline form-inline" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
|
||||
<button class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Create a new gift card" %}</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.secret layout="control" %}
|
||||
{% bootstrap_field form.value layout="control" %}
|
||||
{% bootstrap_field form.currency layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Issued gift cards" %}
|
||||
</h1>
|
||||
{% if giftcards|length == 0 and not filter_form.filtered %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You haven't issued any gift cards yet. You can either set up a product in an event shop to sell gift cards,
|
||||
or you can manually issue gift cards.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default btn-lg"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<form class="row filter-form" action="" method="get">
|
||||
<div class="col-md-10 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.query 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>
|
||||
<span class="hidden-md">
|
||||
{% trans "Filter" %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p>
|
||||
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}</a>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<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></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for g in giftcards %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=g.id %}">
|
||||
<strong>{{ g.secret }}</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ g.issuance|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td class="text-right">
|
||||
{{ g.cached_value|money:g.currency }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=g.id %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -22,6 +22,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer permissions" %}</legend>
|
||||
{% bootstrap_field form.can_create_events layout="control" %}
|
||||
{% bootstrap_field form.can_manage_gift_cards layout="control" %}
|
||||
{% bootstrap_field form.can_change_teams layout="control" %}
|
||||
{% bootstrap_field form.can_change_organizer_settings layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -75,6 +75,9 @@ urlpatterns = [
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
|
||||
name='organizer.display'),
|
||||
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>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
|
||||
name='organizer.webhook.add'),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Max, Min, ProtectedError
|
||||
from django.db.models import Count, DecimalField, Max, Min, ProtectedError, Sum
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.forms import inlineformset_factory
|
||||
from django.http import JsonResponse
|
||||
@@ -21,14 +22,17 @@ from django.views.generic import (
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.models import Device, Organizer, Team, TeamInvite, User
|
||||
from pretix.base.models import (
|
||||
Device, GiftCard, Organizer, Team, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.event import Event, EventMetaProperty
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.forms.filter import OrganizerFilterForm
|
||||
from pretix.control.forms.filter import GiftCardFilterForm, OrganizerFilterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
DeviceForm, EventMetaPropertyForm, OrganizerDeleteForm, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||
DeviceForm, EventMetaPropertyForm, GiftCardCreateForm, OrganizerDeleteForm,
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
WebHookForm,
|
||||
)
|
||||
from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
|
||||
@@ -337,7 +341,7 @@ class OrganizerCreate(CreateView):
|
||||
ret = super().form_valid(form)
|
||||
t = Team.objects.create(
|
||||
organizer=form.instance, name=_('Administrators'),
|
||||
all_events=True, can_create_events=True, can_change_teams=True,
|
||||
all_events=True, can_create_events=True, can_change_teams=True, can_manage_gift_cards=True,
|
||||
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
||||
)
|
||||
@@ -898,3 +902,109 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
|
||||
def get_queryset(self):
|
||||
return self.webhook.calls.order_by('-datetime')
|
||||
|
||||
|
||||
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = GiftCard
|
||||
template_name = 'pretixcontrol/organizers/giftcards.html'
|
||||
permission = 'can_manage_gift_cards'
|
||||
context_object_name = 'giftcards'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.issued_gift_cards.annotate(
|
||||
cached_value=Sum('transactions__value')
|
||||
)
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return GiftCardFilterForm(data=self.request.GET, request=self.request)
|
||||
|
||||
|
||||
class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
template_name = 'pretixcontrol/organizers/giftcard.html'
|
||||
permission = 'can_manage_gift_cards'
|
||||
context_object_name = 'card'
|
||||
|
||||
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 post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if 'value' in request.POST:
|
||||
try:
|
||||
value = DecimalField().to_python(request.POST.get('value'))
|
||||
except ValidationError:
|
||||
messages.error(request, _('Your input was invalid, please try again.'))
|
||||
else:
|
||||
if self.object.value + value < Decimal('0.00'):
|
||||
messages.error(request, _('Gift cards are not allowed to have negative values.'))
|
||||
else:
|
||||
self.object.transactions.create(
|
||||
value=value
|
||||
)
|
||||
self.object.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
data={
|
||||
'value': value
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
messages.success(request, _('The manual transaction has been saved.'))
|
||||
return redirect(reverse(
|
||||
'control:organizer.giftcard',
|
||||
kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
'giftcard': self.object.pk
|
||||
}
|
||||
))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
template_name = 'pretixcontrol/organizers/giftcard_create.html'
|
||||
permission = 'can_manage_gift_cards'
|
||||
form_class = GiftCardCreateForm
|
||||
success_url = 'invalid'
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
any_event = self.request.organizer.events.first()
|
||||
kwargs['initial'] = {
|
||||
'currency': any_event.currency if any_event else settings.DEFAULT_CURRENCY
|
||||
}
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The gift card has been created and can now be used.'))
|
||||
form.instance.issuer = self.request.organizer
|
||||
super().form_valid(form)
|
||||
form.instance.transactions.create(
|
||||
value=form.cleaned_data['value']
|
||||
)
|
||||
form.instance.log_action('pretix.giftcards.transaction.manual', user=self.request.user, data={
|
||||
'value': form.cleaned_data['value']
|
||||
})
|
||||
return redirect(reverse(
|
||||
'control:organizer.giftcard',
|
||||
kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'giftcard': self.object.pk
|
||||
}
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user