Skip to content

Commit

Permalink
Implement voucher per event and for all events of an organizer | Crea…
Browse files Browse the repository at this point in the history
…te Voucher (#473)

* Implement voucher for invoise create/update pages

* Implement invoice voucher delete view

* show currency in invoice voucher page update

* optimize import

* change migration file name

* fix isort, flake8

---------

Co-authored-by: lcduong <[email protected]>
  • Loading branch information
odkhang and lcduong authored Dec 18, 2024
1 parent 6746eef commit 87dc76f
Show file tree
Hide file tree
Showing 12 changed files with 516 additions and 2 deletions.
73 changes: 73 additions & 0 deletions src/pretix/base/migrations/0006_create_invoice_voucher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Generated by Django 5.1.3 on 2024-11-12 08:04

from django.db import migrations, models

import pretix.base.models.base
import pretix.base.models.vouchers


class Migration(migrations.Migration):

dependencies = [
("pretixbase", "0005_page_alter_cachedcombinedticket_id_and_more"),
]

operations = [
migrations.CreateModel(
name="InvoiceVoucher",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
(
"code",
models.CharField(
db_index=True,
default=pretix.base.models.vouchers.generate_code,
max_length=255,
unique=True,
),
),
("max_usages", models.PositiveIntegerField(default=1)),
("redeemed", models.PositiveIntegerField(default=0)),
(
"budget",
models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
(
"valid_until",
models.DateTimeField(blank=True, db_index=True, null=True),
),
("price_mode", models.CharField(default="none", max_length=100)),
(
"value",
models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("created_by", models.CharField(default="system", max_length=50)),
("updated_at", models.DateTimeField(auto_now=True)),
("updated_by", models.CharField(default="system", max_length=50)),
(
"limit_events",
models.ManyToManyField(
related_name="invoice_vouchers", to="pretixbase.event"
),
),
(
"limit_organizer",
models.ManyToManyField(
related_name="invoice_vouchers", to="pretixbase.organizer"
),
),
],
options={
"verbose_name": "Invoice Voucher",
"verbose_name_plural": "Invoice Vouchers",
"ordering": ("code",),
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]
80 changes: 80 additions & 0 deletions src/pretix/base/models/vouchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,3 +503,83 @@ def budget_used(self):
]
).aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00')
return ops


class InvoiceVoucher(LoggedModel):
PRICE_MODES = (
('none', _('No effect')),
('set', _('Set product price to')),
('subtract', _('Subtract from product price')),
('percent', _('Reduce product price by (%)')),
)
code = models.CharField(
verbose_name=_("Voucher code"),
max_length=255, default=generate_code,
db_index=True,
validators=[MinLengthValidator(5)],
unique=True
)
max_usages = models.PositiveIntegerField(
verbose_name=_("Maximum usages"),
help_text=_("Number of times this voucher can be redeemed."),
default=1
)
redeemed = models.PositiveIntegerField(
verbose_name=_("Redeemed"),
default=0
)
budget = models.DecimalField(
verbose_name=_("Maximum discount budget"),
help_text=_("This is the maximum monetary amount that will be "
"discounted using this voucher across all usages."),
decimal_places=2, max_digits=10,
null=True, blank=True
)
valid_until = models.DateTimeField(
blank=True, null=True, db_index=True,
verbose_name=_("Valid until")
)
price_mode = models.CharField(
verbose_name=_("Price mode"),
max_length=100,
choices=PRICE_MODES,
default='none'
)
value = models.DecimalField(
verbose_name=_("Voucher value"),
decimal_places=2, max_digits=10, null=True, blank=True,
)

limit_events = models.ManyToManyField(
'Event',
verbose_name=_("Limit to events"),
blank=True,
related_name='invoice_vouchers'
)

limit_organizer = models.ManyToManyField(
'Organizer',
verbose_name=_("Limit to Organizer"),
blank=True,
related_name='invoice_vouchers'
)

created_at = models.DateTimeField(auto_now_add=True)
created_by = models.CharField(max_length=50, default="system")
updated_at = models.DateTimeField(auto_now=True)
updated_by = models.CharField(max_length=50, default="system")

class Meta:
verbose_name = _("Invoice Voucher")
verbose_name_plural = _("Invoice Vouchers")
ordering = ('code', )

def __str__(self):
return self.code

def is_active(self):
if self.redeemed >= self.max_usages:
return False
if self.valid_until and self.valid_until < now():
return False
return True
Empty file.
67 changes: 67 additions & 0 deletions src/pretix/control/forms/admin/vouchers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled

from pretix.base.forms import I18nModelForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Event, Organizer
from pretix.base.models.vouchers import InvoiceVoucher
from pretix.control.forms import SplitDateTimeField


class InvoiceVoucherForm(I18nModelForm):
event_effect = forms.ModelMultipleChoiceField(
queryset=Event.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Event effect"),
help_text=_("The voucher will only be valid for the selected events.")
)
organizer_effect = forms.ModelMultipleChoiceField(
queryset=Organizer.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Organizer effect"),
help_text=_("The voucher will be valid for all events under the selected organizers.")
)

class Meta:
model = InvoiceVoucher
localized_fields = '__all__'
fields = [
'code', 'valid_until', 'value', 'max_usages', 'price_mode', 'budget', 'event_effect', 'organizer_effect'
]
field_classes = {
'valid_until': SplitDateTimeField,
}
widgets = {
'valid_until': SplitDateTimePickerWidget(),
}

def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
super().__init__(*args, **kwargs)
if instance:
self.fields['event_effect'].initial = instance.limit_events.all()
self.fields['organizer_effect'].initial = instance.limit_organizer.all()
with scopes_disabled():
self.fields['event_effect'].queryset = Event.objects.all()
self.fields['organizer_effect'].queryset = Organizer.objects.all()

def clean(self):
data = super().clean()
return data

def save(self, commit=True):
instance = super().save(commit=False)

if commit:
instance.save()

instance.limit_events.set(self.cleaned_data.get('event_effect', []))
instance.limit_organizer.set(self.cleaned_data.get('organizer_effect', []))

if commit:
self.save_m2m()

return instance
6 changes: 6 additions & 0 deletions src/pretix/control/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,12 @@ def get_admin_navigation(request):
},
]
},
{
'label': _('Vouchers'),
'url': reverse('control:admin.vouchers'),
'active': 'vouchers' in url.url_name,
'icon': 'tags',
},
{
'label': _('Global settings'),
'url': reverse('control:admin.global.settings'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "pretixcontrol/admin/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete voucher" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete voucher" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the voucher <strong>{{ invoice_voucher }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href='{% url "control:admin.vouchers" %}' class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-delete btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends "pretixcontrol/admin/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Voucher" %}{% endblock %}
{% block content %}
<h1>{% trans "Voucher" %}</h1>
{% if voucher.redeemed %}
<div class="alert alert-warning">
{% trans "This voucher already has been used. It is not recommended to modify it." %}
</div>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<div class="row">
<div class="col-xs-12 col-lg-10">
<fieldset>
<legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.code layout="control" %}
{% bootstrap_field form.max_usages layout="control" %}
{% bootstrap_field form.valid_until layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label" >{% trans "Price effect" %}</label>
<div class="col-md-5">
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.value show_label=False form_group_class="" %}
</div>
</div>
{% bootstrap_field form.budget addon_after=currency layout="control" %}
{% bootstrap_field form.event_effect layout="control" %}
{% bootstrap_field form.organizer_effect layout="control" %}
</fieldset>
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends "pretixcontrol/admin/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% load money %}
{% block title %}{% trans "Vouchers" %}{% endblock %}
{% block content %}
<h1>{% trans "Vouchers" %}</h1>
{% if vouchers|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't created any vouchers yet.
{% endblocktrans %}
</p>

<a href='{% url "control:admin.vouchers.add" %}'
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
</div>
{% else %}
<p>
<a href='{% url "control:admin.vouchers.add" %}'
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
</p>
<form action='{% url "control:admin.vouchers" %}' method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>
{% trans "Voucher 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 "Redemptions" %}
<a href="?{% url_replace request 'ordering' '-redeemed' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'redeemed' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Expiry" %}
<a href="?{% url_replace request 'ordering' '-valid_until' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'valid_until' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for v in vouchers %}
<tr>
<td>
{% if not v.is_active %}
<del>
{% endif %}
<strong><a href='{% url "control:admin.voucher" voucher=v.id %}'>{{ v.code }}</a></strong>
{% if not v.is_active %}
</del>
{% endif %}
</td>
<td>
{{ v.redeemed }} / {{ v.max_usages }}
</td>
<td>{{ v.valid_until|date }}</td>
<td class="text-right flip">
<a href='{% url "control:admin.voucher.delete" voucher=v.id %}' class="btn btn-delete btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}
6 changes: 6 additions & 0 deletions src/pretix/control/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='admin.global.settings'),
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='admin.global.update'),
url(r'^global/message/$', global_settings.MessageView.as_view(), name='admin.global.message'),
url(r'^vouchers/$', admin.VoucherList.as_view(), name='admin.vouchers'),
url(r'^vouchers/add$', admin.VoucherCreate.as_view(), name='admin.vouchers.add'),
url(r'^vouchers/(?P<voucher>\d+)/$', admin.VoucherUpdate.as_view(), name='admin.voucher'),
url(r'^vouchers/(?P<voucher>\d+)/delete$', admin.VoucherDelete.as_view(),
name='admin.voucher.delete'),

url(r'^global/sso/$', global_settings.SSOView.as_view(), name='admin.global.sso'),
url(r'^global/sso/(?P<pk>\d+)/delete/$', global_settings.DeleteOAuthApplicationView.as_view(), name='admin.global.sso.delete'),
url(r'^pages/$', pages.PageList.as_view(), name="admin.pages"),
Expand Down
Loading

0 comments on commit 87dc76f

Please sign in to comment.