Skip to content

Commit

Permalink
Implement voucher per event and for all events of an organizer: apply…
Browse files Browse the repository at this point in the history
… voucher in billing setting (#488)

* Apply voucher

* Add migration file, apply voucher for invoice

* Add pydantic model, minor refactor

* Fix spelling

* Add None case for validation

* Fix space

* Add pydantic req, edit validation error

* Add status paid for 0 ticket_fee invoice

* Update migration file, invoice billing model

* Change pdf format

* Add missing colon

* Resolve conversations
  • Loading branch information
HungNgien authored Jan 17, 2025
1 parent d75c72c commit 1827f82
Show file tree
Hide file tree
Showing 11 changed files with 639 additions and 311 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 5.1.4 on 2024-12-26 08:14

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pretixbase', '0006_create_invoice_voucher'),
]

operations = [
migrations.AddField(
model_name='billinginvoice',
name='final_ticket_fee',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
migrations.AddField(
model_name='billinginvoice',
name='voucher_discount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
migrations.AddField(
model_name='organizerbillingmodel',
name='invoice_voucher',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='billing', to='pretixbase.invoicevoucher'),
),
migrations.AddField(
model_name='billinginvoice',
name='voucher_price_mode',
field=models.CharField(max_length=20, null=True),
),
migrations.AddField(
model_name='billinginvoice',
name='voucher_value',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
)
]
5 changes: 3 additions & 2 deletions src/pretix/base/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
cachedticket_name, generate_secret,
)
from .organizer import (
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
Organizer, Organizer_SettingsStore, OrganizerBillingModel, Team,
TeamAPIToken, TeamInvite,
)
from .seating import Seat, SeatCategoryMapping, SeatingPlan
from .tax import TaxRule
from .vouchers import Voucher
from .vouchers import InvoiceVoucher, Voucher
from .waitinglist import WaitingListEntry
6 changes: 6 additions & 0 deletions src/pretix/base/models/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from pretix.base.models import LoggedModel

from .choices import PriceModeChoices


class BillingInvoice(LoggedModel):
STATUS_PENDING = "n"
Expand All @@ -30,6 +32,10 @@ class BillingInvoice(LoggedModel):
currency = models.CharField(max_length=3)

ticket_fee = models.DecimalField(max_digits=10, decimal_places=2)
final_ticket_fee = models.DecimalField(max_digits=10, decimal_places=2, default=0)
voucher_discount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
voucher_price_mode = models.CharField(max_length=20, null=True, blank=True, choices=PriceModeChoices.choices)
voucher_value = models.DecimalField(max_digits=10, decimal_places=2, default=0)
payment_method = models.CharField(max_length=20, null=True, blank=True)
paid_datetime = models.DateTimeField(null=True, blank=True)
note = models.TextField(null=True, blank=True)
Expand Down
9 changes: 9 additions & 0 deletions src/pretix/base/models/choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.db import models
from django.utils.translation import gettext_lazy as _


class PriceModeChoices(models.TextChoices):
NONE = 'none', _('No effect')
SET = 'set', _('Set product price to')
SUBTRACT = 'subtract', _('Subtract from product price')
PERCENT = 'percent', _('Reduce product price by (%)')
7 changes: 7 additions & 0 deletions src/pretix/base/models/organizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,13 @@ class OrganizerBillingModel(models.Model):
verbose_name=_("Tax ID"),
)

invoice_voucher = models.ForeignKey(
"pretixbase.InvoiceVoucher",
on_delete=models.CASCADE,
related_name="billing",
null=True
)

stripe_customer_id = models.CharField(
max_length=255,
verbose_name=_("Stripe Customer ID"),
Expand Down
45 changes: 29 additions & 16 deletions src/pretix/base/models/vouchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from ..decimal import round_decimal
from .base import LoggedModel
from .choices import PriceModeChoices
from .event import Event, SubEvent
from .items import Item, ItemVariation, Quota
from .orders import Order, OrderPosition
Expand Down Expand Up @@ -81,12 +82,6 @@ class Voucher(LoggedModel):
* You need to either select a quota or an item
* If you select an item that has variations but do not select a variation, you cannot set block_quota
"""
PRICE_MODES = (
('none', _('No effect')),
('set', _('Set product price to')),
('subtract', _('Subtract from product price')),
('percent', _('Reduce product price by (%)')),
)

event = models.ForeignKey(
Event,
Expand Down Expand Up @@ -144,8 +139,8 @@ class Voucher(LoggedModel):
price_mode = models.CharField(
verbose_name=_("Price mode"),
max_length=100,
choices=PRICE_MODES,
default='none'
choices=PriceModeChoices.choices,
default=PriceModeChoices.NONE
)
value = models.DecimalField(
verbose_name=_("Voucher value"),
Expand Down Expand Up @@ -506,12 +501,6 @@ def budget_used(self):


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,
Expand Down Expand Up @@ -542,8 +531,8 @@ class InvoiceVoucher(LoggedModel):
price_mode = models.CharField(
verbose_name=_("Price mode"),
max_length=100,
choices=PRICE_MODES,
default='none'
choices=PriceModeChoices.choices,
default=PriceModeChoices.NONE
)
value = models.DecimalField(
verbose_name=_("Voucher value"),
Expand Down Expand Up @@ -583,3 +572,27 @@ def is_active(self):
if self.valid_until and self.valid_until < now():
return False
return True

def calculate_price(self, original_price: Decimal, max_discount: Decimal=None, event: Event=None) -> Decimal:
"""
Returns how the price given in original_price would be modified if this
voucher is applied, i.e. replaced by a different price or reduced by a
certain percentage. If the voucher does not modify the price, the
original price will be returned.
"""
if self.value is not None:
if self.price_mode == 'set':
p = self.value
elif self.price_mode == 'subtract':
p = max(original_price - self.value, Decimal('0.00'))
elif self.price_mode == 'percent':
p = round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
else:
p = original_price
places = settings.CURRENCY_PLACES.get(event.currency, 2)
if places < 2:
p = p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
if max_discount is not None:
p = max(p, original_price - max_discount)
return p
return original_price
41 changes: 35 additions & 6 deletions src/pretix/control/forms/organizer_forms/organizer_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pretix.base.forms import I18nModelForm
from pretix.base.models.organizer import Organizer, OrganizerBillingModel
from pretix.base.models.vouchers import InvoiceVoucher
from pretix.helpers.countries import CachedCountries, get_country_name
from pretix.helpers.stripe_utils import (
create_stripe_customer, update_customer_info,
Expand Down Expand Up @@ -44,6 +45,7 @@ class Meta:
"country",
"preferred_language",
"tax_id",
"invoice_voucher"
]

primary_contact_name = forms.CharField(
Expand Down Expand Up @@ -132,6 +134,14 @@ class Meta:
required=False,
)

invoice_voucher = forms.CharField(
label=_("Invoice Voucher"),
help_text=_("If you have a voucher code, enter it here."),
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
required=False,
)

def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop("organizer", None)
self.warning_message = None
Expand Down Expand Up @@ -162,6 +172,25 @@ def validate_vat_number(self, country_code, vat_number):
result = pyvat.is_vat_number_format_valid(vat_number, country_code)
return result

def clean_invoice_voucher(self):
voucher_code = self.cleaned_data['invoice_voucher']
if not voucher_code:
return None

voucher_instance = InvoiceVoucher.objects.filter(code=voucher_code).first()
if not voucher_instance:
raise forms.ValidationError("Voucher code not found!")

if not voucher_instance.is_active():
raise forms.ValidationError("The voucher code has either expired or reached its usage limit.")

if voucher_instance.limit_organizer.exists():
limit_organizer = voucher_instance.limit_organizer.values_list("id", flat=True)
if self.organizer.id not in limit_organizer:
raise forms.ValidationError("Voucher code is not valid for this organizer!")

return voucher_instance

def clean(self):
cleaned_data = super().clean()
country_code = cleaned_data.get("country")
Expand All @@ -174,14 +203,16 @@ def clean(self):
self.add_error("tax_id", _("Invalid VAT number for {}".format(country_name)))

def save(self, commit=True):
def set_attribute(instance):
for field in self.Meta.fields:
setattr(instance, field, self.cleaned_data[field])

instance = OrganizerBillingModel.objects.filter(
organizer_id=self.organizer.id
).first()

if instance:
for field in self.Meta.fields:
setattr(instance, field, self.cleaned_data[field])

set_attribute(instance)
if commit:
update_customer_info(
instance.stripe_customer_id,
Expand All @@ -191,9 +222,7 @@ def save(self, commit=True):
instance.save()
else:
instance = OrganizerBillingModel(organizer_id=self.organizer.id)
for field in self.Meta.fields:
setattr(instance, field, self.cleaned_data[field])

set_attribute(instance)
if commit:
stripe_customer = create_stripe_customer(
email=self.cleaned_data.get("primary_contact_email"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h1>{% trans "Billing settings" %}</h1>
{% bootstrap_field form.city layout="control" %}
{% bootstrap_field form.country layout="control" %}
{% bootstrap_field form.tax_id layout="control" %}
{% bootstrap_field form.invoice_voucher layout="control" %}
{% bootstrap_field form.preferred_language layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
Expand Down
Loading

0 comments on commit 1827f82

Please sign in to comment.