Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement voucher per event and for all events of an organizer: apply voucher in billing setting #488

Open
wants to merge 10 commits into
base: development
Choose a base branch
from
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ dependencies = [
'eventyay-paypal @ git+https://[email protected]/fossasia/eventyay-tickets-paypal.git@master',
'django_celery_beat==2.7.0',
'cron-descriptor==1.4.5',
'django-allauth[socialaccount]==65.3.0'
'django-allauth[socialaccount]==65.3.0',
'pydantic==2.10.4'
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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'),
)
]
2 changes: 2 additions & 0 deletions src/pretix/base/models/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ 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)
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
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
24 changes: 24 additions & 0 deletions src/pretix/base/models/vouchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,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
8 changes: 5 additions & 3 deletions src/pretix/eventyay_common/billing_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,17 @@ def generate_invoice_pdf(billing_invoice, organizer_billing_info):
Paragraph("#", row_header_style),
Paragraph("DESCRIPTION", row_header_style),
Paragraph("PRICE", row_header_style),
Paragraph("DISCOUNT", row_header_style),
Paragraph("QUANTITY", row_header_style),
Paragraph("AMOUNT", row_header_style),
],
[
"1",
"Ticket fee for " + f"{billing_invoice.monthly_bill.strftime('%B %Y')}",
f"{billing_invoice.ticket_fee} {billing_invoice.currency}",
f"{billing_invoice.voucher_discount} {billing_invoice.currency}",
"1",
f"{billing_invoice.ticket_fee} {billing_invoice.currency}",
f"{billing_invoice.final_ticket_fee} {billing_invoice.currency}",
],
]
item_table = Table(
Expand All @@ -195,12 +197,12 @@ def generate_invoice_pdf(billing_invoice, organizer_billing_info):

# Footer Totals Section
totals_data = [
["SUBTOTAL", f"{billing_invoice.ticket_fee}"],
["SUBTOTAL", f"{billing_invoice.final_ticket_fee}"],
["TAX", "0"],
[
Paragraph("<b>GRAND TOTAL</b>", bold_style),
Paragraph(
f"<b>{billing_invoice.ticket_fee} {billing_invoice.currency}</b>",
f"<b>{billing_invoice.final_ticket_fee} {billing_invoice.currency}</b>",
bold_style,
),
],
Expand Down
8 changes: 8 additions & 0 deletions src/pretix/eventyay_common/schemas/billing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from decimal import Decimal

from pydantic import BaseModel, Field


class CollectBillingResponse(BaseModel):
status: bool = Field(description="Status of collecting billing invoice", default=False)
voucher_discount: Decimal = Field(default=Decimal('0.00'))
Loading
Loading