From 5194e893a7a82884cd1985f9ea61dfc25d5e39a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20D=C3=BCster?= Date: Sat, 9 Sep 2023 17:54:26 +0200 Subject: [PATCH] Enable submission of repayment requests --- build/requirements/requirements.txt | 1 + sipa/blueprints/usersuite.py | 91 ++++++++++++++++++++++++++++- sipa/forms.py | 85 +++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/build/requirements/requirements.txt b/build/requirements/requirements.txt index 44d334b8..24d25150 100644 --- a/build/requirements/requirements.txt +++ b/build/requirements/requirements.txt @@ -24,3 +24,4 @@ recurring_ical_events~=1.0.2b0 cachetools~=5.2.0 python-dotenv~=0.21.0 pydantic~=2.4.2 +schwifty~=2023.6.0 diff --git a/sipa/blueprints/usersuite.py b/sipa/blueprints/usersuite.py index b4c7b528..80cbc89b 100644 --- a/sipa/blueprints/usersuite.py +++ b/sipa/blueprints/usersuite.py @@ -3,6 +3,8 @@ from collections import OrderedDict import logging from datetime import datetime +from decimal import Decimal +from schwifty import IBAN from babel.numbers import format_currency from flask import Blueprint, render_template, url_for, redirect, flash, abort, request, current_app @@ -14,7 +16,8 @@ from sipa.forms import ContactForm, ChangeMACForm, ChangeMailForm, \ ChangePasswordForm, flash_formerrors, HostingForm, \ PaymentForm, ActivateNetworkAccessForm, TerminateMembershipForm, \ - TerminateMembershipConfirmForm, ContinueMembershipForm + TerminateMembershipConfirmForm, ContinueMembershipForm, \ + RequestRepaymentForm, RequestRepaymentConfirmForm from sipa.mail import send_usersuite_contact_mail from sipa.model.fancy_property import ActiveProperty from sipa.utils import password_changeable, subscribe_to_status_page @@ -613,3 +616,89 @@ def reset_wifi_password(): return render_template('generic_form.html', page_title=gettext("Neues WLAN Passwort"), form_args=form_args) + + +@bp_usersuite.route("/request-repayment", methods=["GET", "POST"]) +@login_required +def request_repayment(): + """ + Request a repayment of excess membership contributions + """ + + form = RequestRepaymentForm() + + if form.validate_on_submit(): + return redirect( + url_for( + ".request_repayment_confirm", + beneficiary=form.beneficiary.data, + iban=form.iban.data, + amount=form.amount.data, + ) + ) + elif form.is_submitted(): + flash_formerrors(form) + + form_args = { + "form": form, + "cancel_to": url_for(".index"), + "submit_text": gettext("Weiter"), + } + + return render_template( + "generic_form.html", + page_title=gettext("Rücküberweisung beantragen"), + form_args=form_args, + ) + + +@bp_usersuite.route("/request-repayment/confirm", methods=["GET", "POST"]) +@login_required +def request_repayment_confirm(): + """ + Request a repayment of excess membership contributions + """ + + beneficiary = request.args.get("beneficiary", None) + iban = request.args.get("iban", None, lambda x: IBAN(x, validate_bban=True)) + amount = request.args.get("amount", None, lambda x: Decimal(x)) + + form = RequestRepaymentConfirmForm() + + if None not in (beneficiary, iban, amount): + balance = current_user.finance_information.balance.raw_value + iban_str = str(iban) + + form.beneficiary.data = beneficiary + # Insert a space every 4 characters of the IBAN + form.iban.data = " ".join( + iban_str[i : i + 4] for i in range(0, len(iban_str), 4) + ) + form.bic.data = str(iban.bic) + form.bank.data = iban.bank_name + form.amount.data = amount + form.estimated_balance.data = balance - amount + else: + return redirect(url_for(".request_repayment")) + + if form.validate_on_submit(): + # TODO Prevent submission of multiple requests for repayment, + # requires changes in Pycroft + + # TODO Send request to Pycroft + flash(gettext("Rücküberweisungsantrag erfolgreich abgesendet."), "success") + return redirect(url_for(".index")) + elif form.is_submitted(): + flash_formerrors(form) + + form_args = { + "form": form, + "cancel_text": gettext("Zurück"), + "cancel_to": url_for(".request_repayment"), + } + + return render_template( + "generic_form.html", + page_title=gettext("Rücküberweisung beantragen - Bestätigen"), + form_args=form_args, + ) diff --git a/sipa/forms.py b/sipa/forms.py index 2329fc29..808a9c92 100644 --- a/sipa/forms.py +++ b/sipa/forms.py @@ -1,11 +1,14 @@ import re from datetime import date +from decimal import Decimal from operator import itemgetter from flask_babel import gettext, lazy_gettext from flask import flash from flask_login import current_user from flask_wtf import FlaskForm +from schwifty import IBAN +from schwifty.exceptions import SchwiftyException from werkzeug.local import LocalProxy from wtforms import ( BooleanField, @@ -16,6 +19,7 @@ TextAreaField, IntegerField, DateField, + DecimalField, ) from wtforms.validators import ( DataRequired, @@ -275,6 +279,87 @@ class ChangeMACForm(FlaskForm): ) +def validate_iban(form, field: StringField) -> None: + try: + IBAN(field.data, validate_bban=True) + except SchwiftyException as e: + # TODO Figure out decent error messages + raise ValidationError(e.__doc__) from e + +def validate_amount(form, field: DecimalField) -> None: + if current_user.finance_information.balance.raw_value < field.data: + raise ValidationError(lazy_gettext("Keine ausreichende Guthabendeckung vorhanden!")) + + +class RequestRepaymentForm(FlaskForm): + beneficiary = StrippedStringField( + label=lazy_gettext("Empfänger"), + validators=[DataRequired(lazy_gettext("Empfänger nicht angegeben!"))], + ) + + iban = StrippedStringField( + label=lazy_gettext("IBAN"), + validators=[DataRequired(lazy_gettext("IBAN nicht angegeben!")), validate_iban], + ) + + amount = ( + DecimalField( + label=lazy_gettext("Betrag (EUR)"), + validators=[ + DataRequired(lazy_gettext("Betrag nicht angegeben!")), + NumberRange( + min=Decimal('0.01'), + message=lazy_gettext("Betrag muss mindestens 0,01 € betragen!"), + ), + validate_amount, + ], + ) + ) + + +class RequestRepaymentConfirmForm(FlaskForm): + beneficiary = StrippedStringField( + label=lazy_gettext("Empfänger"), + render_kw={'readonly': True}, + validators=[DataRequired(lazy_gettext("Empfänger nicht angegeben!"))], + ) + + iban = StrippedStringField( + label=lazy_gettext("IBAN"), + render_kw={'readonly': True}, + validators=[DataRequired(lazy_gettext("IBAN nicht angegeben!")), validate_iban], + ) + + bic = StrippedStringField( + label=lazy_gettext("BIC"), + render_kw={'readonly': True}, + ) + + bank = StrippedStringField( + label=lazy_gettext("Bank"), + render_kw={'readonly': True}, + ) + + amount = ( + DecimalField( + label=lazy_gettext("Betrag (EUR)"), + render_kw={'readonly': True}, + validators=[ + DataRequired(lazy_gettext("Betrag nicht angegeben!")), + NumberRange( + min=Decimal('0.01'), + message=lazy_gettext("Betrag muss mindestens 0,01 € betragen!"), + ), + validate_amount, + ], + ) + ) + + estimated_balance = DecimalField( + label=lazy_gettext("Kontostand nach Rücküberweisung (EUR)"), + render_kw={'readonly': True}, + ) + class ActivateNetworkAccessForm(FlaskForm): password = PasswordField( label=lazy_gettext("Passwort"),