Skip to content

Commit

Permalink
Organiser profile billing settings option missing (fossasia#411)
Browse files Browse the repository at this point in the history
* Implement billing settings form for organizer

* Fix flake8 in pipeline

* implement create and save payment_information

* Fix isort, flake8 in pipeline

* implement payment-information-v1

* src/pretix/control/views/organizer_views/organizer_view.py

* Code refactoring

* Code refactoring

* Remove payment_information attribute

* Implement tax validation

* Update code

* Update code

* Fix flake8 in pipeline

* Add pyvat package

* Fix flake8 in pipeline

* Latest code

* Fix flake8 in pipeline

* Add logger error

* Fix flake8 in pipeline

* Fix conflict pretix base migration

* Update pretix base migration

* Add logging information and modify error logging

* Add comment,save tax_id, show error and sucess message

* Update code

* Fix flake8 in pipeline

* move validation to clean method and move get_country_name to countries helper
  • Loading branch information
odkhang authored Nov 18, 2024
1 parent 5df752e commit 27cc074
Show file tree
Hide file tree
Showing 14 changed files with 1,065 additions and 3 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dependencies = [
'django-sso==3.0.2',
'PyJWT~=2.8.0',
'exhibitors @ git+https://github.com/fossasia/eventyay-tickets-exhibitors.git@master',
'pyvat==1.3.18',
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 5.1.2 on 2024-10-31 09:30

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


class Migration(migrations.Migration):

dependencies = [
("pretixbase", "0003_event_is_video_creation_and_more"),
]

operations = [
migrations.CreateModel(
name="OrganizerBillingModel",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("primary_contact_name", models.CharField(max_length=255)),
("primary_contact_email", models.EmailField(max_length=255)),
("company_or_organization_name", models.CharField(max_length=255)),
("address_line_1", models.CharField(max_length=255)),
("address_line_2", models.CharField(max_length=255)),
("city", models.CharField(max_length=255)),
("zip_code", models.CharField(max_length=255)),
("country", models.CharField(max_length=255)),
("preferred_language", models.CharField(max_length=255)),
("tax_id", models.CharField(max_length=255)),
("stripe_customer_id", models.CharField(max_length=255, null=True)),
(
"stripe_payment_method_id",
models.CharField(max_length=255, null=True),
),
("stripe_setup_intent_id", models.CharField(max_length=255, null=True)),
(
"organizer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="billing",
to="pretixbase.organizer",
),
),
],
),
]
89 changes: 89 additions & 0 deletions src/pretix/base/models/organizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,92 @@ def get_events_with_permission(self, permission, request=None):
return self.get_events_with_any_permission()
else:
return self.team.organizer.events.none()


class OrganizerBillingModel(models.Model):
"""
Billing model - support billing information for organizer
"""

organizer = models.ForeignKey(
"Organizer", on_delete=models.CASCADE, related_name="billing"
)

primary_contact_name = models.CharField(
max_length=255,
verbose_name=_("Primary Contact Name"),
)

primary_contact_email = models.EmailField(
max_length=255,
verbose_name=_("Primary Contact Email"),
)

company_or_organization_name = models.CharField(
max_length=255,
verbose_name=_("Company or Organization Name"),
)

address_line_1 = models.CharField(
max_length=255,
verbose_name=_("Address Line 1"),
)

address_line_2 = models.CharField(
max_length=255,
verbose_name=_("Address Line 2"),
)

city = models.CharField(
max_length=255,
verbose_name=_("City"),
)

zip_code = models.CharField(
max_length=255,
verbose_name=_("Zip Code"),
)

country = models.CharField(
max_length=255,
verbose_name=_("Country"),
)

preferred_language = models.CharField(
max_length=255,
verbose_name=_("Preferred Language"),
)

tax_id = models.CharField(
max_length=255,
verbose_name=_("Tax ID"),
)

stripe_customer_id = models.CharField(
max_length=255,
verbose_name=_("Stripe Customer ID"),
blank=True,
null=True,
)

stripe_payment_method_id = models.CharField(
max_length=255,
verbose_name=_("Payment Method"),
blank=True,
null=True,
)

stripe_setup_intent_id = models.CharField(
max_length=255,
verbose_name=_("Setup Intent ID"),
blank=True,
null=True,
)

def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
self.organizer.cache.clear()

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.organizer.cache.clear()
182 changes: 181 additions & 1 deletion src/pretix/control/forms/organizer_forms/organizer_form.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import pyvat
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _

from pretix.base.forms import I18nModelForm
from pretix.base.models.organizer import Organizer
from pretix.base.models.organizer import Organizer, OrganizerBillingModel
from pretix.helpers.countries import CachedCountries, get_country_name
from pretix.helpers.stripe_utils import (
create_stripe_customer, update_customer_info,
)


class OrganizerForm(I18nModelForm):
Expand All @@ -22,3 +28,177 @@ def clean_slug(self):
code='duplicate_slug',
)
return slug


class BillingSettingsForm(forms.ModelForm):
class Meta:
model = OrganizerBillingModel
fields = [
"primary_contact_name",
"primary_contact_email",
"company_or_organization_name",
"address_line_1",
"address_line_2",
"zip_code",
"city",
"country",
"preferred_language",
"tax_id",
]

primary_contact_name = forms.CharField(
label=_("Primary Contact Name"),
help_text=_(
"Please provide your name or the name of the person responsible for this account in your organization."
),
required=True,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

primary_contact_email = forms.EmailField(
label=_("Primary Contact Email"),
help_text=_(
"We will use this email address for all communication related to your contract and billing, "
"as well as for important updates about your account and our services."
),
required=True,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

company_or_organization_name = forms.CharField(
label=_("Company or Organization Name"),
help_text=_("Enter your organization’s legal name."),
required=True,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

address_line_1 = forms.CharField(
label=_("Address Line 1"),
help_text=_("Street address or P.O. box."),
required=True,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

address_line_2 = forms.CharField(
label=_("Address Line 2"),
help_text=_("Apartment, suite, unit, etc. (optional)."),
required=False,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

zip_code = forms.CharField(
label=_("Zip Code"),
help_text=_("Enter your postal code."),
required=True,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

city = forms.CharField(
label=_("City"),
help_text=_("Enter your city."),
required=True,
max_length=255,
widget=forms.TextInput(attrs={"placeholder": ""}),
)

country = forms.ChoiceField(
label=_("Country"),
help_text=_("Select your country."),
required=True,
choices=CachedCountries(),
initial="US",
)

preferred_language = forms.ChoiceField(
label=_("Preferred Language for Correspondence"),
help_text=_("Select your preferred language for all communication."),
required=True,
)

tax_id = forms.CharField(
label=_("Tax ID (e.g., VAT, GST)"),
help_text=_(
"If you are located in the EU, please provide your VAT ID. "
"Without this, we will need to charge VAT on our services and will not be able to issue reverse charge invoices."
),
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
super().__init__(*args, **kwargs)
selected_languages = [
(code, name)
for code, name in settings.LANGUAGES
if code in self.organizer.settings.locales
]
self.fields["preferred_language"].choices = selected_languages
self.fields["preferred_language"].initial = self.organizer.settings.locale
self.set_initial_data()

def set_initial_data(self):
billing_settings = OrganizerBillingModel.objects.filter(
organizer_id=self.organizer.id
).first()

if billing_settings:
for field in self.Meta.fields:
self.initial[field] = getattr(billing_settings, field, "")

def validate_vat_number(self, country_code, vat_number):
if country_code not in pyvat.VAT_REGISTRIES:
country_name = get_country_name(country_code)
self.warning_message = _("VAT number validation is not supported for {}".format(country_name))
return True
result = pyvat.is_vat_number_format_valid(vat_number, country_code)
return result

def clean(self):
cleaned_data = super().clean()
country_code = cleaned_data.get("country")
vat_number = cleaned_data.get("tax_id")

if vat_number and country_code:
country_name = get_country_name(country_code)
is_valid_vat_number = self.validate_vat_number(country_code, vat_number)
if not is_valid_vat_number:
self.add_error("tax_id", _("Invalid VAT number for {}".format(country_name)))

def save(self, commit=True):
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])

if commit:
update_customer_info(
instance.stripe_customer_id,
email=self.cleaned_data.get("primary_contact_email"),
name=self.cleaned_data.get("primary_contact_name"),
)
instance.save()
else:
instance = OrganizerBillingModel(organizer_id=self.organizer.id)
for field in self.Meta.fields:
setattr(instance, field, self.cleaned_data[field])

if commit:
stripe_customer = create_stripe_customer(
email=self.cleaned_data.get("primary_contact_email"),
name=self.cleaned_data.get("primary_contact_name")
)
instance.stripe_customer_id = stripe_customer.id
instance.save()
return instance
8 changes: 8 additions & 0 deletions src/pretix/control/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,14 @@ def get_organizer_navigation(request):
}),
'active': 'organizer.webhook' in url.url_name,
'icon': 'bolt',
},
{
"label": _("Billing settings"),
"url": reverse(
"control:organizer.settings.billing",
kwargs={"organizer": request.organizer.slug},
),
"active": url.url_name == "organizer.settings.billing",
}
]
})
Expand Down
1 change: 1 addition & 0 deletions src/pretix/control/templates/pretixcontrol/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<head>
<title>{% block title %}{% endblock %}{% if url_name != "index" %} :: {% endif %}
{{ django_settings.INSTANCE_NAME }}</title>
<script src="https://js.stripe.com/v3/"></script>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixcontrol/scss/main.scss" %}" />
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
Expand Down
Loading

0 comments on commit 27cc074

Please sign in to comment.