1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-21 11:18:27 +02:00

Setup payment interface

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello
2024-02-18 22:36:01 +01:00
parent 7c9083a6b8
commit 4d157b2bd7
8 changed files with 366 additions and 131 deletions

View File

@ -219,7 +219,7 @@ class VolunteerRegistrationForm(forms.ModelForm):
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)
class PaymentForm(forms.ModelForm):
class PaymentAdminForm(forms.ModelForm):
"""
Indicate payment information
"""
@ -228,7 +228,6 @@ class PaymentForm(forms.ModelForm):
self.fields["valid"].widget.choices[0] = ('unknown', _("Pending"))
def clean_receipt(self):
print(self.files)
if "receipt" in self.files:
file = self.files["receipt"]
if file.size > 2e6:
@ -241,7 +240,7 @@ class PaymentForm(forms.ModelForm):
cleaned_data = super().clean()
if "type" in cleaned_data and cleaned_data['type'] in ["scholarship", "bank_transfer"] \
and "receipt" not in self.files and not self.instance.scholarship_file:
and "receipt" not in self.files and not self.instance.receipt:
self.add_error("receipt", _("You must upload your receipt."))
return cleaned_data
@ -249,3 +248,37 @@ class PaymentForm(forms.ModelForm):
class Meta:
model = Payment
fields = ('type', 'receipt', 'additional_information', 'valid',)
class PaymentForm(forms.ModelForm):
"""
Indicate payment information
"""
def __init__(self, payment_type, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['type'].widget = forms.HiddenInput(attrs={'value': payment_type})
self.fields['receipt'].required = payment_type in ["scholarship", "bank_transfer"]
self.fields['additional_information'].required = payment_type in ["other"]
def clean_receipt(self):
if "receipt" in self.files:
file = self.files["receipt"]
if file.size > 2e6:
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
return self.cleaned_data["receipt"]
def clean(self):
cleaned_data = super().clean()
if "type" in cleaned_data and cleaned_data['type'] in ["scholarship", "bank_transfer"] \
and "receipt" not in self.files and not self.instance.receipt:
self.add_error("receipt", _("You must upload your receipt."))
return cleaned_data
class Meta:
model = Payment
fields = ('type', 'receipt', 'additional_information',)

View File

@ -113,7 +113,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(blank=True, choices=[('', 'No payment'), ('helloasso', 'Hello Asso'), ('scholarship', 'Scholarship'), ('bank_transfer', 'Bank transfer'), ('other', 'Other (please indicate)'), ('free', 'The tournament is free')], default='', max_length=16, verbose_name='type')),
('scholarship_file', models.FileField(blank=True, default='', help_text='only if you have a scholarship.', upload_to=registration.models.get_scholarship_filename, verbose_name='scholarship file')),
('scholarship_file', models.FileField(blank=True, default='', help_text='only if you have a scholarship.', upload_to=registration.models.get_receipt_filename, verbose_name='scholarship file')),
('additional_information', models.TextField(blank=True, default='', help_text='To help us to find your payment.', verbose_name='additional information')),
('valid', models.BooleanField(default=False, null=True, verbose_name='valid')),
('registration', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='registration.participantregistration', verbose_name='registration')),

View File

@ -60,7 +60,7 @@ class Migration(migrations.Migration):
blank=True,
default="",
help_text="only if you have a scholarship or if you chose a bank transfer.",
upload_to=registration.models.get_scholarship_filename,
upload_to=registration.models.get_receipt_filename,
verbose_name="receipt",
),
),

View File

@ -492,8 +492,8 @@ class VolunteerRegistration(Registration):
verbose_name_plural = _("volunteer registrations")
def get_scholarship_filename(instance, filename):
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
def get_receipt_filename(instance, filename):
return f"authorization/receipt/receipt_{instance.id}"
class Payment(models.Model):
@ -526,7 +526,7 @@ class Payment(models.Model):
max_length=16,
choices=[
('', _("No payment")),
('helloasso', "Hello Asso"),
('helloasso', _("Credit card")),
('scholarship', _("Scholarship")),
('bank_transfer', _("Bank transfer")),
('other', _("Other (please indicate)")),
@ -546,7 +546,7 @@ class Payment(models.Model):
receipt = models.FileField(
verbose_name=_("receipt"),
help_text=_("only if you have a scholarship or if you chose a bank transfer."),
upload_to=get_scholarship_filename,
upload_to=get_receipt_filename,
blank=True,
default="",
)

View File

@ -3,8 +3,140 @@
{% load crispy_forms_filters i18n %}
{% block content %}
<div class="alert alert-warning">
Le formulaire de paiement est temporairement désactivé. Il sera accessible d'ici quelques jours.
</div>
{% if payment.valid is False %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed with amount=payment.amount %}
You must pay {{ amount }} € for your registration.
{% endblocktrans %}
{% if payment.grouped %}
{% blocktrans trimmed %}
This price includes the registrations of all members of your team.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
This price includes only your own registration.
You are exempt from payment if you have a scholarship,
but you must then send us a proof of your scholarship.
{% endblocktrans %}
{% endif %}
</p>
<p>
{% if payment.grouped %}
{% blocktrans trimmed %}
You want finally that each member pays its own registration? Then click on the button:
{% endblocktrans %}
<div class="text-center">
<button class="btn btn-warning">
<i class="fas fa-user"></i> {% trans "Back to single payments" %}
</button>
</div>
{% else %}
{% blocktrans trimmed %}
You want to pay for the registrations of all members of your team,
or your school will pay for all registrations? Then click on the button:
{% endblocktrans %}
<div class="text-center">
<button class="btn btn-warning">
<i class="fas fa-users"></i> {% trans "Group the payments of my team" %}
</button>
</div>
{% endif %}
</p>
</div>
<div class="card">
<div class="card-header">
<nav>
<div class="nav nav-tabs card-header-tabs" id="payment-method-tab" role="tablist">
<button class="nav-link active" id="credit-card-tab" data-bs-toggle="tab"
data-bs-target="#credit-card" type="button" role="tab"
aria-controls="credit-card" aria-selected="true">
<i class="fas fa-credit-card"></i> {% trans "Credit card" %}
</button>
<button class="nav-link" id="bank-transfer-tab" data-bs-toggle="tab"
data-bs-target="#bank-transfer" type="button" role="tab"
aria-controls="bank-transfer" aria-selected="true">
<i class="fas fa-money-check"></i> {% trans "Bank transfer" %}
</button>
{% if not payment.grouped %}
<button class="nav-link" id="scholarship-tab" data-bs-toggle="tab"
data-bs-target="#scholarship" type="button" role="tab"
aria-controls="scholarship" aria-selected="true">
<i class="fas fa-file-invoice"></i> {% trans "I have a scholarship" %}
</button>
{% endif %}
<button class="nav-link" id="other-tab" data-bs-toggle="tab"
data-bs-target="#other" type="button" role="tab"
aria-controls="other" aria-selected="true">
<i class="fas fa-question"></i> {% trans "Other" %}
</button>
</div>
</nav>
</div>
<div class="card-body">
<div class="tab-content" id="payment-form">
<div class="tab-pane fade show active" id="credit-card" role="tabpanel" aria-labelledby="credit-card-tab">
Le paiement par carte bancaire s'effectue via Hello Asso. Pour cela, vous pouvez cliquer sur
le bouton ci-dessous, qui vous redirigera vers la page de paiement sécurisée de Hello Asso.
La validation du paiement sera ensuite faite automatiquement, sous quelques minutes.
Si un tiers doit payer pour vous (parents, lycée,…), vous pouvez lui transmettre le lien pour
payer pour vous.
<div class="text-center">
<a href="#" class="btn btn-primary">
<i class="fas fa-credit-card"></i> Aller sur la page Hello Asso
</a>
</div>
</div>
<div class="tab-pane fade" id="bank-transfer" role="tabpanel" aria-labelledby="bank-transfer-tab">
<form id="bank-transfer-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ bank_transfer_form|crispy }}
<input type="submit" class="btn btn-primary" />
</form>
</div>
<div class="tab-pane fade" id="scholarship" role="tabpanel" aria-labelledby="scholarship-tab">
<form id="scholarship-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ scholarship_form|crispy }}
<input type="submit" class="btn btn-primary" />
</form>
</div>
<div class="tab-pane fade" id="other" role="tabpanel" aria-labelledby="other-tab">
<form id="other-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ other_form|crispy }}
<input type="submit" class="btn btn-primary" />
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock content %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
if (document.location.hash) {
// Open the tab of the tournament that is present in the hash
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
if ('#' + elem.getAttribute('aria-controls') === document.location.hash.toLowerCase()) {
elem.click()
}
})
}
// When a tab is opened, add the tournament name in the hash
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
elem => elem.addEventListener(
'click', () => document.location.hash = '#' + elem.getAttribute('aria-controls')))
})
</script>
{% endblock %}

View File

@ -29,7 +29,7 @@ from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, \
ParentalAuthorizationForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
VaccineSheetForm, VolunteerRegistrationForm
VaccineSheetForm, VolunteerRegistrationForm, PaymentAdminForm
from .models import ParticipantRegistration, Payment, Registration, StudentRegistration
from .tables import RegistrationTable
@ -443,7 +443,7 @@ class InstructionsTemplateView(AuthorizationTemplateView):
class PaymentUpdateView(LoginRequiredMixin, UpdateView):
model = Payment
form_class = PaymentForm
form_class = PaymentAdminForm
def dispatch(self, request, *args, **kwargs):
if not self.request.user.is_authenticated or \
@ -453,22 +453,33 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get_form(self, form_class=None):
form = super().get_form(form_class)
if not self.request.user.registration.is_admin:
form.fields["type"].widget.choices = list(form.fields["type"].widget.choices)[:-1]
del form.fields["valid"]
return form
def get_context_data(self, **kwargs):
context = super().get_context_data()
context['title'] = _("Update payment")
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
data=self.request.POST or None,
instance=self.object)
context['scholarship_form'] = PaymentForm(payment_type='scholarship',
data=self.request.POST or None,
instance=self.object)
context['other_form'] = PaymentForm(payment_type='other',
data=self.request.POST or None,
instance=self.object)
return context
def form_valid(self, form):
if not self.request.user.registration.is_admin:
form.instance.valid = None
form.instance.valid = None
old_instance = Payment.objects.get(pk=self.object.pk)
if old_instance.receipt:
old_instance.receipt.delete()
old_instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("registration:user_detail", args=(self.object.registrations.first().user.pk,))
class PhotoAuthorizationView(LoginRequiredMixin, View):
"""
@ -562,9 +573,9 @@ class ParentalAuthorizationView(LoginRequiredMixin, View):
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
class ScholarshipView(LoginRequiredMixin, View):
class ReceiptView(LoginRequiredMixin, View):
"""
Display the sent scholarship paper.
Display the sent payment receipt or scholarship notification.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
@ -573,14 +584,14 @@ class ScholarshipView(LoginRequiredMixin, View):
raise Http404
payment = Payment.objects.get(receipt__endswith=filename)
user = request.user
if not (user.registration in payment.registrations or user.registration.is_admin):
if not (user.registration in payment.registrations.all() or user.registration.is_admin):
raise PermissionDenied
# Guess mime type of the file
mime = Magic(mime=True)
mime_type = mime.from_file(path)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
# Replace file name
true_file_name = _("Scholarship attestation of {user}.{ext}").format(user=str(user.registration), ext=ext)
true_file_name = _("Payment receipt of {user}.{ext}").format(user=str(user.registration), ext=ext)
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)