From a09d14627ffb3c6059236c4c7906fd7306176993 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 11 Jul 2026 01:53:22 +0200 Subject: [PATCH 01/39] BadBIC model --- amelie/personal_tab/migrations/0011_badbic.py | 26 +++++++++++++++++++ amelie/personal_tab/models.py | 19 ++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 amelie/personal_tab/migrations/0011_badbic.py diff --git a/amelie/personal_tab/migrations/0011_badbic.py b/amelie/personal_tab/migrations/0011_badbic.py new file mode 100644 index 00000000..2a4f1f68 --- /dev/null +++ b/amelie/personal_tab/migrations/0011_badbic.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.12 on 2026-07-10 23:49 + +import localflavor.generic.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('personal_tab', '0010_alter_pendingpostoken_token'), + ] + + operations = [ + migrations.CreateModel( + name='BadBIC', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bic', localflavor.generic.models.BICField(max_length=11, unique=True, verbose_name='BIC')), + ], + options={ + 'verbose_name': 'known bad BIC', + 'verbose_name_plural': 'known bad BICs', + 'ordering': ['bic'], + }, + ), + ] diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 2e4099dd..777263c4 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -949,6 +949,25 @@ def get_absolute_url(self): return reverse('personal_tab:reversal_transaction_detail', args=[self.pk]) +class BadBIC(models.Model): + """ + A known bad SEPA BIC of a bank that is in the SDD Core list but does not in practice accept direct debits + + Please note that processing properties of this model may be subject to privacy regulations. Refer to + https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. + """ + + bic = BICField(verbose_name=_l('BIC'), unique=True) + + def __str__(self): + return self.bic + + class Meta: + ordering = ['bic'] + verbose_name = _l('known bad BIC') + verbose_name_plural = _l('known bad BICs') + + def get_sentinel_person() -> Person: return Person.objects.get(pk=settings.ANONIMIZATION_SENTINEL_PERSON_ID) From 28a0551cb48c6aae3c5615488ad234a992dd8d80 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 11 Jul 2026 01:56:14 +0200 Subject: [PATCH 02/39] Bad BIC model admin --- amelie/personal_tab/admin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/amelie/personal_tab/admin.py b/amelie/personal_tab/admin.py index d140de62..49acccd0 100644 --- a/amelie/personal_tab/admin.py +++ b/amelie/personal_tab/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from amelie.personal_tab.models import Declaration, DiscountPeriod, Discount, DiscountCredit, Transaction, \ +from amelie.personal_tab.models import BadBIC, Declaration, DiscountPeriod, Discount, DiscountCredit, Transaction, \ CustomTransaction, ActivityTransaction, CookieCornerTransaction, AlexiaTransaction, ContributionTransaction, \ Article, Category, RFIDCard, AuthorizationType, Authorization, Amendment, DebtCollectionAssignment, \ DebtCollectionBatch, \ @@ -304,6 +304,12 @@ def has_add_permission(self, request): return False +class BadBICAdmin(admin.ModelAdmin): + list_display = ('id', 'bic') + search_fields = ['bic'] + ordering = ['bic'] + + class DiscountPeriodAdmin(admin.ModelAdmin): list_display = ('id', 'description_nl', 'description_en', 'begin', 'end', 'ledger_account_number', 'balance_account_number') @@ -380,6 +386,7 @@ class DeclarationAdmin(admin.ModelAdmin): admin.site.register(DebtCollectionBatch, DebtCollectionBatchAdmin) admin.site.register(DebtCollectionInstruction, DebtCollectionInstructionAdmin) admin.site.register(Reversal, ReversalAdmin) +admin.site.register(BadBIC, BadBICAdmin) admin.site.register(DiscountPeriod, DiscountPeriodAdmin) admin.site.register(Discount, DiscountAdmin) admin.site.register(DiscountCredit, DiscountCreditAdmin) From c9d421ae6e4d0fa798232b1477f68ab8802a66bc Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 11 Jul 2026 02:38:58 +0200 Subject: [PATCH 03/39] Adding BIC to BadBIC list if a reversal with DNOR reason is processed --- amelie/personal_tab/admin.py | 2 +- amelie/personal_tab/debt_collection.py | 9 +++++++-- amelie/personal_tab/migrations/0011_badbic.py | 3 ++- amelie/personal_tab/models.py | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/amelie/personal_tab/admin.py b/amelie/personal_tab/admin.py index 49acccd0..674e05fd 100644 --- a/amelie/personal_tab/admin.py +++ b/amelie/personal_tab/admin.py @@ -305,7 +305,7 @@ def has_add_permission(self, request): class BadBICAdmin(admin.ModelAdmin): - list_display = ('id', 'bic') + list_display = ('id', 'bic', 'date') search_fields = ['bic'] ordering = ['bic'] diff --git a/amelie/personal_tab/debt_collection.py b/amelie/personal_tab/debt_collection.py index 53011ac9..8c839243 100644 --- a/amelie/personal_tab/debt_collection.py +++ b/amelie/personal_tab/debt_collection.py @@ -7,8 +7,8 @@ from django.utils.translation import gettext as _ from amelie.members.models import Person, Membership, PaymentType, Payment -from amelie.personal_tab.models import Transaction, DebtCollectionInstruction, DebtCollectionBatch, \ - DebtCollectionTransaction, ContributionTransaction, ReversalTransaction, Reversal, Amendment +from amelie.personal_tab.models import BadBIC, Transaction, DebtCollectionInstruction, DebtCollectionBatch, \ + DebtCollectionTransaction, ContributionTransaction, ReversalTransaction, Amendment from amelie.tools.encodings import normalize_to_ascii @@ -333,6 +333,11 @@ def process_reversal(reversal, actor): description=description, membership=ct.membership) cct.save() + # If the reversal reason is 'DNOR', the BIC of the authorization must be added to the Bad BIC list + if reversal.reason is 'DNOR': + bad_bic = BadBIC(bic=instruction.authorization.bic) + bad_bic.save() + def edit_reversal(reversal, actor): """ diff --git a/amelie/personal_tab/migrations/0011_badbic.py b/amelie/personal_tab/migrations/0011_badbic.py index 2a4f1f68..06639e3a 100644 --- a/amelie/personal_tab/migrations/0011_badbic.py +++ b/amelie/personal_tab/migrations/0011_badbic.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.12 on 2026-07-10 23:49 +# Generated by Django 5.2.12 on 2026-07-11 00:21 import localflavor.generic.models from django.db import migrations, models @@ -16,6 +16,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('bic', localflavor.generic.models.BICField(max_length=11, unique=True, verbose_name='BIC')), + ('date', models.DateTimeField(auto_now_add=True, verbose_name='Date added')), ], options={ 'verbose_name': 'known bad BIC', diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 777263c4..0ea57137 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -958,6 +958,7 @@ class BadBIC(models.Model): """ bic = BICField(verbose_name=_l('BIC'), unique=True) + date = models.DateTimeField(verbose_name=_l('Date added'), auto_now_add=True) def __str__(self): return self.bic From 6512a24a9bc8f07c8b51ac836ed355c42e73a403 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 11 Jul 2026 03:24:31 +0200 Subject: [PATCH 04/39] Added confirmation screen template and logic in ajax view --- amelie/members/ajax_views.py | 11 ++++++- .../templates/person_confirm_mandate.html | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 amelie/members/templates/person_confirm_mandate.html diff --git a/amelie/members/ajax_views.py b/amelie/members/ajax_views.py index ab38cd4b..cb6a8a37 100644 --- a/amelie/members/ajax_views.py +++ b/amelie/members/ajax_views.py @@ -17,7 +17,7 @@ from amelie.members.models import Payment, Committee, Function, Membership, Employee, PaymentType, Person, Student, \ StudyPeriod, Preference, PreferenceCategory from amelie.members.query_views import filter_member_list_public -from amelie.personal_tab.models import Authorization +from amelie.personal_tab.models import Authorization, BadBIC from amelie.personal_tab.pos_views import require_cookie_corner_pos from amelie.tools.decorators import require_ajax, require_board, require_actief, require_committee from amelie.tools.mixins import RequireCommitteeMixin @@ -246,8 +246,17 @@ def person_mandate_new(request, id): obj = get_object_or_404(Person, id=id) if request.method == "POST": form = MandateForm(request.POST) + if form.is_valid(): + # If the filled BIC is in the Bad BIC list, ask for a confirmation. + if BadBIC.objects.filter(bic=form.cleaned_data['bic']).exists(): + if 'confirm' in request.POST: + pass + else: + return render(request, "person_confirm_mandate.html", locals()) + mandate: Authorization = form.save(commit=False) + mandate = form.save(commit=False) mandate.person = obj mandate.save() if form.cleaned_data.get('send_signature_request'): diff --git a/amelie/members/templates/person_confirm_mandate.html b/amelie/members/templates/person_confirm_mandate.html new file mode 100644 index 00000000..fa4b83db --- /dev/null +++ b/amelie/members/templates/person_confirm_mandate.html @@ -0,0 +1,29 @@ +{% load i18n %} +
+

{% trans 'Confirm BIC for mandate' %}

+ +
+ {% csrf_token %} + {% for field in form.visible_fields %} + + {% endfor %} + + +

{% blocktrans with bic=form.cleaned_data.bic iban=form.cleaned_data.iban %} + The BIC for this mandate is on the list of known BICs that do not accept SEPA Direct Debits. + Are you sure you want to add a mandate with the following IBAN: {{ iban }} and BIC: {{ bic }}? + {% endblocktrans %}

+ +

{% blocktrans %} + Only do this if you are sure that this bank accepts SEPA Direct Debits. + {% endblocktrans %}

+ + +
+
From d5290a8c8d3ed2c7424e8be26d178c1a89093e07 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 11 Jul 2026 20:20:03 +0200 Subject: [PATCH 05/39] Changed ibancalculator link to the correct link --- .../members/templates/includes/registration/authorizations.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amelie/members/templates/includes/registration/authorizations.html b/amelie/members/templates/includes/registration/authorizations.html index 708a72a9..641ecdce 100644 --- a/amelie/members/templates/includes/registration/authorizations.html +++ b/amelie/members/templates/includes/registration/authorizations.html @@ -68,7 +68,7 @@

{% trans 'Mandates' %}

{{ wizard.form.bic|form_bootstrapify:"noplaceholder" }}
- {% trans 'Your BIC-code. Not required for Dutch bank accounts. For example "ABNANL2A". Find your BIC by clicking here (opens in new tab).' %} + {% trans 'Your BIC-code. Not required for Dutch bank accounts. For example "ABNANL2A". Find your BIC by clicking here (opens in new tab).' %}
From 7283e959a8b26c80d7078b0dd4f720420352cb72 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 11 Jul 2026 20:23:52 +0200 Subject: [PATCH 06/39] Added Bad BIC list check to registration form(s) --- amelie/members/forms.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/amelie/members/forms.py b/amelie/members/forms.py index 9f5bdcef..95ba9f03 100644 --- a/amelie/members/forms.py +++ b/amelie/members/forms.py @@ -25,7 +25,7 @@ from amelie.members.models import Department, PaymentType, Committee, CommitteeCategory, DogroupGeneration, \ Function, Membership, MembershipType, Employee, Person, Student, Study, StudyPeriod, Preference, \ LANGUAGE_CHOICES, UnverifiedEnrollment -from amelie.personal_tab.models import Authorization, AuthorizationType +from amelie.personal_tab.models import Authorization, AuthorizationType, BadBIC from amelie.tools.logic import current_academic_year_with_holidays, current_association_year from amelie.tools.validators import CheckDigitValidator from amelie.tools.widgets import DateSelector, MemberSelect @@ -660,7 +660,7 @@ def clean_student_number(self): return self.cleaned_data["student_number"] -def clean_iban_and_bic(iban, bic): +def clean_iban_and_bic(iban, bic, registration=False): iban = str(iban).strip() bic = str(bic).strip() @@ -676,11 +676,13 @@ def clean_iban_and_bic(iban, bic): raise forms.ValidationError(_l('BIC has to be entered for foreign bankaccounts.')) elif iban[4:8] in settings.COOKIE_CORNER_BANK_CODES: bic = settings.COOKIE_CORNER_BANK_CODES[iban[4:8]] - # BIC is known, so cleaning is done - return iban, bic else: raise forms.ValidationError(_l('BIC could not be generated, please enter it yourself.')) + # If the BIC is in the Bad BICs list and it is filled in via the RegistrationForm, do not accept it. + if registration and BadBIC.objects.filter(bic=bic): + raise forms.ValidationError(_l('The bank associated with your BIC does not accept SEPA Direct Debits.')) + if bic[4:6] != iban[:2]: raise forms.ValidationError(_l('The BIC country does not match the IBAN country.')) @@ -711,5 +713,5 @@ def clean_iban_and_bic(iban, bic): raise IOError(u"Reading file at {} failed (run 'python manage.py update_bic_csv' to create it if it does not exist): {}" .format(os.path.join(settings.MEDIA_ROOT, 'data/bic_list.csv'), e)) - # If the BIC is not in the list, the BIC is not from a SEPA country - raise forms.ValidationError(_l("BIC is not from a SEPA country")) + # If the BIC is not in the list, the BIC bank does not accept SEPA Direct Debits + raise forms.ValidationError(_l("The bank associated with your BIC does not accept SEPA Direct Debits.")) From 29230cea2398c99ffcd39cddbba76706c60c4674 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Thu, 16 Jul 2026 00:00:30 +0200 Subject: [PATCH 07/39] Adding logic to remove BIC from Bad BIC list if a reversal that triggered it is edited/deleted --- amelie/personal_tab/admin.py | 2 +- amelie/personal_tab/debt_collection.py | 72 +++++++++++++++++-- .../{0011_badbic.py => 0014_badbic.py} | 9 ++- amelie/personal_tab/models.py | 4 +- 4 files changed, 76 insertions(+), 11 deletions(-) rename amelie/personal_tab/migrations/{0011_badbic.py => 0014_badbic.py} (52%) diff --git a/amelie/personal_tab/admin.py b/amelie/personal_tab/admin.py index 674e05fd..c9fc1bad 100644 --- a/amelie/personal_tab/admin.py +++ b/amelie/personal_tab/admin.py @@ -305,7 +305,7 @@ def has_add_permission(self, request): class BadBICAdmin(admin.ModelAdmin): - list_display = ('id', 'bic', 'date') + list_display = ('id', 'bic', 'date_added', 'last_reversal', 'first_reversal') search_fields = ['bic'] ordering = ['bic'] diff --git a/amelie/personal_tab/debt_collection.py b/amelie/personal_tab/debt_collection.py index 8c839243..871229a0 100644 --- a/amelie/personal_tab/debt_collection.py +++ b/amelie/personal_tab/debt_collection.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext as _ from amelie.members.models import Person, Membership, PaymentType, Payment -from amelie.personal_tab.models import BadBIC, Transaction, DebtCollectionInstruction, DebtCollectionBatch, \ +from amelie.personal_tab.models import BadBIC, Reversal, Transaction, DebtCollectionInstruction, DebtCollectionBatch, \ DebtCollectionTransaction, ContributionTransaction, ReversalTransaction, Amendment from amelie.tools.encodings import normalize_to_ascii @@ -333,9 +333,13 @@ def process_reversal(reversal, actor): description=description, membership=ct.membership) cct.save() - # If the reversal reason is 'DNOR', the BIC of the authorization must be added to the Bad BIC list - if reversal.reason is 'DNOR': - bad_bic = BadBIC(bic=instruction.authorization.bic) + # If the reversal reason is DNOR, the BIC of the authorization must be added to the Bad BIC list + if reversal.reason == Reversal.ReversalReasons.DNOR: + bad_bic = BadBIC.objects.filter(bic=instruction.authorization.bic).first() + if not bad_bic: + bad_bic = BadBIC(bic=instruction.authorization.bic, first_reversal=reversal, last_reversal=reversal) + else: + bad_bic.last_reversal = reversal bad_bic.save() @@ -344,6 +348,7 @@ def edit_reversal(reversal, actor): :type reversal: Reversal :type actor: Person """ + instruction = reversal.instruction timezone_amsterdam = timezone.get_default_timezone() reversal_datetime = datetime.datetime.combine(reversal.date, datetime.time(0, 0)).replace(tzinfo=timezone_amsterdam) @@ -352,16 +357,46 @@ def edit_reversal(reversal, actor): rt.added_by = actor rt.save() + # If the reversal reason is DNOR, the BIC of the authorization must be added to the Bad BIC list + if reversal.reason == Reversal.ReversalReasons.DNOR: + bad_bic = BadBIC.objects.filter(bic=instruction.authorization.bic).first() + if not bad_bic: + bad_bic = BadBIC(bic=instruction.authorization.bic, first_reversal=reversal, last_reversal=reversal) + else: + bad_bic.last_reversal = reversal + bad_bic.save() + + # Otherwise, check if there wasn't any BadBIC made because of the reversal before the edit + else: + bad_bic_first = BadBIC.objects.filter(first_reversal=reversal).first() + bad_bic_last = BadBIC.objects.filter(last_reversal=reversal).first() + + # If a BIC exists where this reversal was the first occurence + if bad_bic_first: + # If it was the first and last occurence, we can delete the BIC from the list + if bad_bic_first.last_reversal == reversal: + bad_bic_first.delete() + # Otherwise, we set the first occurence to the last occurence + else: + bad_bic_first.first_reversal = bad_bic_first.last_reversal + bad_bic_first.save() + + # Otherwise, if a BIC exists where this reversal was the last occurence + elif bad_bic_last: + # We set the last occurence to the first occurence + bad_bic_last.last_reversal = bad_bic_last.first_reversal + bad_bic_last.save() + + def delete_reversal(reversal): """ :type reversal: Reversal """ - # First we delete the ReversalTransaction and the Reversal object itself. + # First we delete the ReversalTransaction instruction = reversal.instruction rt = ReversalTransaction.objects.get(reversal=reversal) rt.delete() - reversal.delete() # If there were contribution transactions, we need to reinstate payments and delete reversal contribution transactions. for ct in ContributionTransaction.objects.filter(debt_collection=instruction): @@ -377,6 +412,31 @@ def delete_reversal(reversal): cct.delete() + # If the reversal reason was DNOR, the BIC of the authorization must be removed from the Bad BIC list + if reversal.reason == Reversal.ReversalReasons.DNOR: + bad_bic_first = BadBIC.objects.filter(first_reversal=reversal).first() + bad_bic_last = BadBIC.objects.filter(last_reversal=reversal).first() + + # If a BIC exists where this reversal was the first occurence + if bad_bic_first: + # If it was the first and last occurence, we can delete the BIC from the list + if bad_bic_first.last_reversal == reversal: + bad_bic_first.delete() + # Otherwise, we set the first occurence to the last occurence + else: + bad_bic_first.first_reversal = bad_bic_first.last_reversal + bad_bic_first.save() + + # Otherwise, if a BIC exists where this reversal was the last occurence + elif bad_bic_last: + # We set the last occurence to the first occurence + bad_bic_last.last_reversal = bad_bic_last.first_reversal + bad_bic_last.save() + + # Finally, we delete the Reversal object itself + reversal.delete() + + def process_amendment(authorization, date, iban, bic, reason): """ Process an amendment to an authorization. diff --git a/amelie/personal_tab/migrations/0011_badbic.py b/amelie/personal_tab/migrations/0014_badbic.py similarity index 52% rename from amelie/personal_tab/migrations/0011_badbic.py rename to amelie/personal_tab/migrations/0014_badbic.py index 06639e3a..1e59d5bd 100644 --- a/amelie/personal_tab/migrations/0011_badbic.py +++ b/amelie/personal_tab/migrations/0014_badbic.py @@ -1,5 +1,6 @@ -# Generated by Django 5.2.12 on 2026-07-11 00:21 +# Generated by Django 5.2.12 on 2026-07-15 11:26 +import django.db.models.deletion import localflavor.generic.models from django.db import migrations, models @@ -7,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('personal_tab', '0010_alter_pendingpostoken_token'), + ('personal_tab', '0013_authorization_documenso_id_and_more'), ] operations = [ @@ -16,7 +17,9 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('bic', localflavor.generic.models.BICField(max_length=11, unique=True, verbose_name='BIC')), - ('date', models.DateTimeField(auto_now_add=True, verbose_name='Date added')), + ('date_added', models.DateTimeField(auto_now_add=True, verbose_name='Date added')), + ('first_reversal', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='personal_tab.reversal', verbose_name='First reversal')), + ('last_reversal', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='personal_tab.reversal', verbose_name='Last reversal')), ], options={ 'verbose_name': 'known bad BIC', diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 0ea57137..83b39649 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -958,7 +958,9 @@ class BadBIC(models.Model): """ bic = BICField(verbose_name=_l('BIC'), unique=True) - date = models.DateTimeField(verbose_name=_l('Date added'), auto_now_add=True) + date_added = models.DateTimeField(verbose_name=_l('Date added'), auto_now_add=True) + first_reversal = models.ForeignKey(Reversal, verbose_name=_l("First reversal"), related_name=_l('+'), null=True, on_delete=models.SET_NULL) + last_reversal = models.ForeignKey(Reversal, verbose_name=_l("Last reversal"), related_name=_l('+'), null=True, on_delete=models.SET_NULL) def __str__(self): return self.bic From 764b0ca4fa97c11115d5d7cdf78d824baa04d5ac Mon Sep 17 00:00:00 2001 From: MiniLars Date: Thu, 16 Jul 2026 22:27:58 +0200 Subject: [PATCH 08/39] Added bookkeeping field to declarationform. Selecting a committee here will override the declaration email to the committee declaration email. Also people will now receive a CC of their declaration --- amelie/personal_tab/forms.py | 31 +++++++++++++++---- .../0014_declaration_bookkeeping.py | 20 ++++++++++++ amelie/personal_tab/models.py | 4 +++ .../templates/declaration_form.html | 12 +++++++ amelie/settings/environ.py | 7 +++-- amelie/settings/generic.py | 4 +++ 6 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 amelie/personal_tab/migrations/0014_declaration_bookkeeping.py diff --git a/amelie/personal_tab/forms.py b/amelie/personal_tab/forms.py index 704da178..985872ed 100644 --- a/amelie/personal_tab/forms.py +++ b/amelie/personal_tab/forms.py @@ -395,6 +395,12 @@ def __init__(self, person: Optional[Person], *args, **kwargs): super().__init__(*args, **kwargs) +class MyBookkeepingChoiceField(forms.ModelChoiceField): + # Overriding label_from_instance to show the abbreviation instead of full committee name + def label_from_instance(self, obj: Committee): + return obj.abbreviation + + class DeclarationForm(forms.Form): committee = MyCommitteesFirstChoiceField( person=None, # Will be set in __init__ @@ -441,10 +447,18 @@ class DeclarationForm(forms.Form): required = False, help_text=_l('Select documents to attach to the declaration.'), ) + bookkeeping = MyBookkeepingChoiceField( + queryset=Committee.objects.filter(abbreviation__in=settings.DECLARATION_EMAIL_COMMITTEE_OVERRIDE), + empty_label=("Inter-Actief"), + required=False, + label=_l('Bookkeeping'), + help_text=_l('Select the bookkeeping where the declaration should be sent.\nIf you do not know what this means, do not change it.') + ) def __init__(self, person: Person, *args, **kwargs): super().__init__(*args, **kwargs) + self.person = person # Only show parent committees, sorted by name, but set person to show own committees first self.fields['committee'].person = person @@ -457,7 +471,6 @@ def __init__(self, person: Person, *args, **kwargs): self.fields['iban_choice'].choices = list(set((iban, iban) for iban in person.has_mandate().distinct().order_by('start_date').values_list('iban', flat=True))) self.fields['iban_choice'].choices += [('', _l("-- Other --"))] self.initial['iban_choice'] = self.fields['iban_choice'].choices[0][0] if self.fields['iban_choice'].choices else None - self.person = person def clean_documents(self): documents = self.cleaned_data.get('documents') @@ -526,6 +539,7 @@ def save(self, request): amount = self.cleaned_data.get('amount') description = self.cleaned_data.get('description') documents = self.cleaned_data.get('documents', []) + bookkeeping = self.cleaned_data.get('bookkeeping') # Create database entry for the declaration declaration = Declaration.objects.create( @@ -536,9 +550,13 @@ def save(self, request): amount=amount, description=description, # Saving the document names as a JSON list - document_names=json.dumps([doc.name for doc in documents]) + document_names=json.dumps([doc.name for doc in documents]), + bookkeeping=bookkeeping, ) + # Set the declaration email to committee email if a different bookkeeping was selected, otherwise normal declaration email + email_to = settings.DECLARATION_EMAIL_COMMITTEE_OVERRIDE.get(bookkeeping.abbreviation) if bookkeeping else settings.DECLARATION_EMAIL + # Prepare context for the email context = {'declaration': declaration} @@ -553,9 +571,10 @@ def save(self, request): task = MailTask(template_name='declaration.mail', report_to=settings.EMAIL_REPORT_TO, report_always=False, priority=TaskPriority.MEDIUM) - task.add_recipient(Recipient(tos=[settings.DECLARATION_EMAIL], - context=context, - language='en', - headers={'Reply-To': settings.TREASURER_EMAIL}, attachments=attachments)) + task.add_recipient(Recipient(tos=[email_to], + ccs=[person.email_address], + context=context, + headers={'Reply-To': settings.TREASURER_EMAIL}, + attachments=attachments)) task.send() diff --git a/amelie/personal_tab/migrations/0014_declaration_bookkeeping.py b/amelie/personal_tab/migrations/0014_declaration_bookkeeping.py new file mode 100644 index 00000000..391361a5 --- /dev/null +++ b/amelie/personal_tab/migrations/0014_declaration_bookkeeping.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.12 on 2026-07-16 13:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0023_documenso_id_signed_document'), + ('personal_tab', '0013_authorization_documenso_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='declaration', + name='bookkeeping', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='members.committee', verbose_name='bookkeeping'), + ), + ] diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 2e4099dd..a1efd921 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -1040,6 +1040,10 @@ class Declaration(models.Model): submission_date = models.DateTimeField(auto_now_add=True, blank=False) """Submission timestamp of the declaration.""" + # '+' related_name makes sure no reverse relation is added to Committee + bookkeeping = models.ForeignKey(Committee, verbose_name=_l('bookkeeping'), related_name='+', on_delete=models.PROTECT, blank=True, null=True) + """The committee that the declaration should be sent to.""" + def get_payment_method(self): return dict(self.DECLARATION_PAYMENT_METHODS).get(self.payment_method, self.payment_method) diff --git a/amelie/personal_tab/templates/declaration_form.html b/amelie/personal_tab/templates/declaration_form.html index c97fe97e..c1f28aad 100644 --- a/amelie/personal_tab/templates/declaration_form.html +++ b/amelie/personal_tab/templates/declaration_form.html @@ -132,6 +132,18 @@

{% trans "Declaration Information" %}

{% endif %} + + + {{ form.bookkeeping.label }} + + {{ form.bookkeeping.errors|safe }} +
{{ form.bookkeeping }}
+ {% if form.bookkeeping.help_text %} + {{ form.bookkeeping.help_text|linebreaks }} + {% endif %} + + + diff --git a/amelie/settings/environ.py b/amelie/settings/environ.py index 8383b561..dd5589d8 100644 --- a/amelie/settings/environ.py +++ b/amelie/settings/environ.py @@ -535,11 +535,12 @@ def custom_show_toolbar(request): # Maximum number of files for declarations PERSONAL_TAB_DECLARATION_MAX_FILE_AMOUNT = env.int("PERSONAL_TAB_DECLARATION_MAX_FILE_AMOUNT", default=PERSONAL_TAB_DECLARATION_MAX_FILE_AMOUNT) -# E-mail address for declarations -DECLARATION_EMAIL = env("AMELIE_DECLARATION_EMAIL", default=DECLARATION_EMAIL) +# E-mail addresses for declarations including committees with separate bookkeepings +DECLARATION_EMAIL = env("DECLARATION_EMAIL", default=DECLARATION_EMAIL) +DECLARATION_EMAIL_COMMITTEE_OVERRIDE = env("DECLARATION_EMAILS", default=DECLARATION_EMAIL_COMMITTEE_OVERRIDE) # E-mail address of the Treasurer -TREASURER_EMAIL = env("AMELIE_TREASURER_EMAIL", default=TREASURER_EMAIL) +TREASURER_EMAIL = env("TREASURER_EMAIL", default=TREASURER_EMAIL) ### # Amelie-specific settings diff --git a/amelie/settings/generic.py b/amelie/settings/generic.py index a702998c..93a128f8 100644 --- a/amelie/settings/generic.py +++ b/amelie/settings/generic.py @@ -591,6 +591,10 @@ # Declaration Settings DECLARATION_EMAIL = 'Declarations ' +DECLARATION_EMAIL_COMMITTEE_OVERRIDE = { + "SocCie": "SocCie Declarations ", + "Kick-IT": "Kick-IT Declarations ", +} TREASURER_EMAIL = 'Penningmeester Inter-Actief ' PERSONAL_TAB_DECLARATION_MAX_FILE_SIZE = 20 * 1024 * 1024 # 20 MB total, because email size limit is 25 MB PERSONAL_TAB_DECLARATION_MAX_FILE_AMOUNT = 10 From 8506beb28c703d347ab1217e364c3aca0603c7f0 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 18 Jul 2026 23:08:04 +0200 Subject: [PATCH 09/39] Unnecessary imports --- amelie/personal_tab/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amelie/personal_tab/forms.py b/amelie/personal_tab/forms.py index 704da178..a8d275f5 100644 --- a/amelie/personal_tab/forms.py +++ b/amelie/personal_tab/forms.py @@ -10,7 +10,7 @@ from django import forms from django.conf import settings from django.db import transaction -from django.db.models import Exists, OuterRef, TextChoices +from django.db.models import TextChoices from django.utils import timezone from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _l From 08042d95fb4e98335ae257daf4cf1aaa0b3a7e6a Mon Sep 17 00:00:00 2001 From: MiniLars Date: Sat, 18 Jul 2026 23:49:09 +0200 Subject: [PATCH 10/39] Changing printing refund from CustomTransaction to CookieCornerTransaction with same information as original print transaction. --- amelie/personal_tab/print_views.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/amelie/personal_tab/print_views.py b/amelie/personal_tab/print_views.py index fc52866a..1f3c1719 100644 --- a/amelie/personal_tab/print_views.py +++ b/amelie/personal_tab/print_views.py @@ -12,7 +12,7 @@ from pyipp import IPPConnectionError from amelie.personal_tab.forms import PrintDocumentForm -from amelie.personal_tab.models import PrintLogEntry, Article, CustomTransaction +from amelie.personal_tab.models import CookieCornerTransaction, PrintLogEntry, Article from amelie.tools.decorators import require_superuser from amelie.tools.mixins import RequirePersonalTabAuthorizationOrActiveMemberMixin, RequireBoardMixin @@ -95,12 +95,10 @@ def get_object(self, *args, **kwargs): def _refund_transaction(self): old_transaction = self.object.transaction - CustomTransaction.objects.create( - person=old_transaction.person, - added_by=self.request.user.person, - description=_("Refund for print, transaction #{tid}").format(tid=old_transaction.id), - price=-old_transaction.price, - ) + CookieCornerTransaction.objects.create(price=-old_transaction.price, person=old_transaction.person, + description=_("Refund for print, transaction #{tid}").format(tid=old_transaction.id), + article=old_transaction.article, amount=old_transaction.amount, added_by=self.request.user.person) + self.object.transaction = None self.object.save() messages.success(self.request, _("Print by {person} was refunded successfully.").format(person=self.object.actor)) From 8c4b385d093a4ca5e851eb828fe4a37175793ff9 Mon Sep 17 00:00:00 2001 From: MiniLars Date: Wed, 22 Jul 2026 15:14:51 +0200 Subject: [PATCH 11/39] Moving forgive and mailing buttons to top of page and adding explanation. --- .../templates/unpaid_memberships_year.html | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/amelie/personal_tab/templates/unpaid_memberships_year.html b/amelie/personal_tab/templates/unpaid_memberships_year.html index 2b858339..023164aa 100644 --- a/amelie/personal_tab/templates/unpaid_memberships_year.html +++ b/amelie/personal_tab/templates/unpaid_memberships_year.html @@ -4,11 +4,20 @@ {% block titel %}{% trans 'Unpaid Memberships' %} {{ year }}{% endblock titel %} {% block content %} -
-
-

{% trans 'Unpaid Memberships' %} {{ year }}

+
+
+

{% trans 'Unpaid Memberships' %} {{ year }}

+
+

+ {% trans 'Use these buttons to send a mailing or forgive the membership fee of the selected members.' %} +

+
+
{% for membership_type, membership_list in unpaid_memberships.items %}
@@ -69,8 +78,6 @@

{{ membership_type.0 }}

{% endfor %} - {% trans 'Send mailing' %} - {% trans 'Forgive membership fee' %}

@@ -80,14 +87,15 @@

{{ membership_type.0 }}

- - -{% endblock head %} + -

{% trans 'This enrollment list contains personal data. Please do not leave this lying around somewhere and make sure that it is destroyed after use. If you find this list somewhere, please give it to the board of Inter-Actief.' %}

+

{% trans 'This enrollment list contains personal data. Please do not leave this lying around somewhere and make sure that it is destroyed after use. If you find this list somewhere, please give it to the board of Inter-Actief.' %}

{% trans 'Number of participants' %} {{ number_participants }}

- +
+ - {% for enrollmentoption in obj.enrollmentoption_set.all %} {% endfor %} - - - - + {% for participation in participation_set %} + {% for answer in participation.enrollmentoptionanswer_set.all %} {% endfor %} - @@ -74,21 +86,3 @@

{% trans 'Order totals' %}

✓ {% trans 'Name' %} {{ enrollmentoption.title }}{% trans 'Method of payment' %}{% trans 'Payment' %}
{{ participation.person.incomplete_name }} {{ answer.display_answer }}{{ participation.get_payment_method_display }} - - {% trans 'Deregister' %} - - {% if participation.remark %} - + {% if participation.is_free %} + {% trans 'n.a. (free)' %} + {% else %} + {% with participation.payment_details as details %} + + {% if participation.is_paid %} + {% trans 'Paid' %} + {# Cannot use brackets in jinja templates, so we need to list the same block twice. #} + {% elif details.0 == 'unpaid' and details.1 != None %} + {% trans 'To be debited' %} + {% elif details.0 == 'reversed' %} + {% trans 'To be debited' %} + {% else %} + {% if details.0 == 'unpaid' and details.1 == None %} + {% trans 'Needs manual payment' %} + {% else %} + {% trans 'Not paid yet' %} + {% endif %} + {% endif %} + + {% endwith %} {% endif %}
{% endif %} - -

- {% if request.is_board or not obj.enrollment_full %} - - {% trans 'Enroll people' %} - - {% endif %} - - {% if number_participants %} - - {% trans 'Send mailing' %} - - {% endif %} - - {% if request.is_board %} - {% trans 'View transactions' %} - {% endif %} -

diff --git a/amelie/activities/templates/activity_list.html b/amelie/activities/templates/activity_list.html index f79b4b76..8b2dcbd9 100644 --- a/amelie/activities/templates/activity_list.html +++ b/amelie/activities/templates/activity_list.html @@ -126,11 +126,8 @@

{% trans 'Past activities' % {% trans 'Dutch-only' %} {{ activity.summary }} + href="{{ activity.get_absolute_url }}" + title="{{ activity.description|striptags|truncatewords:30 }}">{{ activity.summary }} {% include "includes/activity_list_photos.html" %} diff --git a/amelie/activities/templates/includes/activity_enrollment.html b/amelie/activities/templates/includes/activity_enrollment.html index 8237b78c..0ef7dda3 100644 --- a/amelie/activities/templates/includes/activity_enrollment.html +++ b/amelie/activities/templates/includes/activity_enrollment.html @@ -118,15 +118,14 @@ {% endif %} {% if obj.can_unenroll or request.person in obj.waiting_participants %} -
- {% csrf_token %} -

- - {% if has_enrollment_options %} - {% trans 'Edit enrollment' %} - {% endif %} -

-
+

+ + {% if request.person in obj.waiting_participants %}{% trans 'Deregister from waiting list' %}{% else %}{% trans 'Deregister' %}{% endif %} + + {% if has_enrollment_options %} + {% trans 'Edit enrollment' %} + {% endif %} +

{% blocktrans with date=obj.enrollment_end|date:"d F Y" time=obj.enrollment_end|date:"H:i" %} You can reverse your enrollment till {{ date }} at {{ time }}. @@ -144,7 +143,6 @@ {% endblocktrans %}

- {% if not obj.can_unenroll and obj.enrollment_open %}

{% blocktrans %} @@ -163,12 +161,11 @@

{% endif %} {% if obj.enrollment_open %} -
- {% csrf_token %} -

- -

-
+

+ + {% if obj.enrollment_full %}{% trans 'Place on waiting list' %}{% else %}{% trans 'Enroll' %}{% endif %} + +

{% endif %} {% endif %} {% else %} diff --git a/amelie/activities/templates/activity_enrollment_overview.html b/amelie/activities/templates/includes/activity_enrollment_overview.html similarity index 57% rename from amelie/activities/templates/activity_enrollment_overview.html rename to amelie/activities/templates/includes/activity_enrollment_overview.html index 5b9bc85f..2c3c3ba3 100644 --- a/amelie/activities/templates/activity_enrollment_overview.html +++ b/amelie/activities/templates/includes/activity_enrollment_overview.html @@ -33,7 +33,7 @@

{% if request.is_board %}
- + {% if only_show_underage %} {% trans 'Show all participants' %} {% else %} @@ -68,110 +68,9 @@ {# This block shows the participants table #}
- - - - {% if request.is_board or is_committee or is_roomduty %} - - - {% else %} - - {% endif %} - - {% if request.is_board or is_committee or is_roomduty %} - {% for enrollmentoption in obj.enrollmentoption_set.all %} - - {% endfor %} - - - {% endif %} - - {% if request.is_board %} - - - {% endif %} - - - - {# confirmed_participation_set will be filtered with only underage participants if this is given in the query parameter #} - {# The table for committee, roomduty and board #} - {% if request.is_board or is_committee or is_roomduty %} - {% for participation in confirmed_participation_set %} - - - - {% for answer in participation.enrollmentoptionanswer_set.all %} - - {% endfor %} - {% if request.is_board or is_roomduty %} - - {% elif is_committee %} - - {% endif %} - {% if request.is_board %} - - - {% endif %} - - {% endfor %} - {% else %} - {# The table for normal members #} - {% for participation in public_participation_set %} - - - - {% endfor %} - - - - {% endif %} - -
- {% trans 'Sequence' %} - - {% trans 'Name' %} - - {% trans 'Name' %} - {{ enrollmentoption.title }}{% trans 'Payment' %}
- {{ forloop.counter }} - - {% if request.is_board %} - {{ participation.person.incomplete_name }} - {% if participation in confirmed_participation_set_turns_18_during_event %}{% endif %} - {% elif deanonymise %} - {{ participation.person.incomplete_name }}{% if participation in confirmed_participation_set_turns_18_during_event %}{% endif %} - {% elif not obj.enrollment_private %} - {% if participation in public_participation_set %} - {{ participation.person.public_enrollment_name }} - {% else %} - {% trans 'Anonymous' %} - {% endif %} - {% if participation in confirmed_participation_set_turns_18_during_event %}{% endif %} - {% else %} - {% trans 'Anonymous' %} - {% endif %} - {{ answer.display_answer }}{% if participation.has_paid %}

{{ participation.get_payment_method_display }}{% else %}

{% trans "Register cash payment" %}{% endif %}

{{ participation.get_payment_method_display }} - - {% trans 'Edit enrollment' %} - - - - {% trans 'Deregister' %} - - {% if participation.remark %} - - {% endif %} -
- {{ participation.person.public_enrollment_name }} -
- - {% blocktranslate count count=anonymous_count %} - ...and {{ count }} Anonymous member. - {% plural %} - ...and {{ count }} Anonymous members. - {% endblocktranslate %} - -
+
+ {% include 'includes/activity_participants_table.html' %} +
{# This block shows the restaurant order #} @@ -240,7 +139,7 @@

{% blocktrans with n=number_waitin {{ enrollmentoption.title }} {% endfor %} - {% trans 'Method of payment' %} + {% trans 'Payment' %} {% if request.is_board or deanonymise %} @@ -267,15 +166,25 @@

{% blocktrans with n=number_waitin {% for answer in participation.enrollmentoptionanswer_set.all %} {{ answer.display_answer }} {% endfor %} - {{ participation.get_payment_method_display }} + + {% if participation.is_free %} + {% trans 'n.a. (free)' %} + {% else %} + {% if participation.person.has_mandate_activities %} + {% trans 'To be debited' %} + {% else %} + {% trans 'Needs manual payment' %} + {% endif %} + {% endif %} + {% if request.is_board or deanonymise %} - + {% trans 'Edit enrollment' %} - + {% trans 'Deregister' %} {% if participation.remark %} diff --git a/amelie/activities/templates/includes/activity_participants_table.html b/amelie/activities/templates/includes/activity_participants_table.html new file mode 100644 index 00000000..586b7263 --- /dev/null +++ b/amelie/activities/templates/includes/activity_participants_table.html @@ -0,0 +1,195 @@ +{% load i18n %} + + + + {% if request.is_board or is_committee or is_roomduty %} + + + {% else %} + + {% endif %} + + {% if request.is_board or is_committee or is_roomduty %} + {% for enrollmentoption in obj.enrollmentoption_set.all %} + + {% endfor %} + + + {% endif %} + + {% if request.is_board %} + + + {% endif %} + + + + {# confirmed_participation_set will be filtered with only underage participants if this is given in the query parameter #} + {# The table for committee, roomduty and board #} + {% if request.is_board or is_committee or is_roomduty %} + {% for participation in confirmed_participation_set %} + + + + {% for answer in participation.enrollmentoptionanswer_set.all %} + + {% endfor %} + {% if request.is_board or is_roomduty %} + + {% elif is_committee %} + + {% endif %} + {% if request.is_board %} + + + {% endif %} + + {% endfor %} + {% else %} + {# The table for normal members #} + {% for participation in public_participation_set %} + + + + {% endfor %} + + + + {% endif %} + +
+ {% trans 'Sequence' %} + + {% trans 'Name' %} + + {% trans 'Name' %} + {{ enrollmentoption.title }}{% trans 'Payment' %}
+ {{ forloop.counter }} + + {% if request.is_board %} + {{ participation.person.incomplete_name }} + {% if participation in confirmed_participation_set_turns_18_during_event %}{% endif %} + {% elif deanonymise %} + {{ participation.person.incomplete_name }}{% if participation in confirmed_participation_set_turns_18_during_event %}{% endif %} + {% elif not obj.enrollment_private %} + {% if participation in public_participation_set %} + {{ participation.person.public_enrollment_name }} + {% else %} + {% trans 'Anonymous' %} + {% endif %} + {% if participation in confirmed_participation_set_turns_18_during_event %}{% endif %} + {% else %} + {% trans 'Anonymous' %} + {% endif %} + {{ answer.display_answer }} + {% if participation.is_free %} + {% trans 'n.a. (free)' %} +   + {% if participation.activitytransaction_set.exists %} + + + + {% endif %} + {% else %} + {% with participation.payment_details as details %} + {% if participation.is_paid %} + {% trans 'Paid' %} + {% else %} + {# Cannot use brackets in jinja templates, so we need to list the same block twice. #} + {% if details.0 == 'unpaid' and details.1 != None %} + {% trans 'To be debited' %} + {% elif details.0 == 'reversed' %} + {% trans 'To be debited' %} + {% else %} + {% trans 'Not paid yet' %} + {% endif %} + {% endif %} +   + + + {% if details.0 == 'no_transaction' %} +
{% trans 'No transaction! Contact the WWW.' %} + {% elif details.0 == 'paid' %} + {% if details.1.settlement_type == "INSTR" %} +
+ {% blocktrans with date=details.1.batch.execution_date|date:"Y-m-d" %} + Debited on {{ date }} + {% endblocktrans %} + + {% elif details.1.settlement_type == "MANUAL" %} +
+ {% blocktrans with method=details.1.payment_method.name date=details.1.payment_date|date:"Y-m-d" %} + {{ method }} on {{ date }} + {% endblocktrans %} + + {% else %} +
+ {% trans 'Unknown payment! Contact the WWW.' %} + + {% endif %} + {% elif details.0 == 'reversed' %} +
+ {% trans 'Previous attempt was reversed' %} + + {% elif details.0 == 'unpaid' and details.1 == None %} +
+ {% trans 'Needs manual payment' %} + + {% endif %} + {% if details.0 == 'unpaid' and details.1 == None %} +
+ {% trans 'Enter manual payment' %} + + {% endif %} + {% endwith %} + {% endif %} +
+ {% if participation.is_free %} + {% trans 'n.a. (free)' %} + {% else %} + {% with participation.payment_details as details %} + {% if participation.is_paid %} + {% trans 'Paid' %} + {# Cannot use brackets in jinja templates, so we need to list the same block twice. #} + {% elif details.0 == 'unpaid' and details.1 != None %} + {% trans 'To be debited' %} + {% elif details.0 == 'reversed' %} + {% trans 'To be debited' %} + {% else %} + {% trans 'Not paid yet' %} + {% if details.0 == 'unpaid' and details.1 == None %} +
+ {% trans 'Needs manual payment' %} + + {% endif %} + {% endif %} + {% endwith %} + {% endif %} +
+ + {% trans 'Edit enrollment' %} + + + + {% trans 'Deregister' %} + + {% if participation.remark %} + + {% endif %} +
+ {{ participation.person.public_enrollment_name }} +
+ + {% blocktranslate count count=anonymous_count %} + ...and {{ count }} Anonymous member. + {% plural %} + ...and {{ count }} Anonymous members. + {% endblocktranslate %} + +
diff --git a/amelie/activities/templates/includes/activity_participation_create_payment.html b/amelie/activities/templates/includes/activity_participation_create_payment.html new file mode 100644 index 00000000..4c0288b2 --- /dev/null +++ b/amelie/activities/templates/includes/activity_participation_create_payment.html @@ -0,0 +1,73 @@ +{% load humanize %} +{% load i18n %} +
+

+ {% blocktrans with participation=object %} + Create manual payment for {{ participation }} + {% endblocktrans %} +

+ +

{% trans 'You are creating a manual payment that will cover the following transactions:' %}

+ +
+ + + + + + + + + + {% for transaction in transactions %} + + + + + + {% empty %} + + + + {% endfor %} + + + + + + +
{% trans 'Timestamp' %}{% trans 'Description' %}{% trans 'Amount' %}
+ {{ transaction.date|date:"d-m-Y H:i" }} + + + {{ transaction.description }} + + + € {{ transaction.price|intcomma }} +
+ {% trans 'No transactions found. This is probably wrong! Please contact the WWW.' %} +
{% trans 'Total' %}€ {{ transactions_total|intcomma }}
+
+
+

{% trans 'Please select the payment method that was used for the payment.' %}

+ +
+ {% csrf_token %} + {{ form }} + +
+ + {% trans 'Cancel' %} + + + + +
+
+
+
diff --git a/amelie/activities/templates/includes/activity_participation_payments.html b/amelie/activities/templates/includes/activity_participation_payments.html new file mode 100644 index 00000000..04920276 --- /dev/null +++ b/amelie/activities/templates/includes/activity_participation_payments.html @@ -0,0 +1,72 @@ +{% load humanize %} +{% load i18n %} +
+ {# Messages to show if we redirected here from the create manual payment view #} + {% if status_msg == "created" %} +
    +
  • + {% trans 'Payment created. Please check the transactions below for correctness.' %} +
  • +
+ {% elif status_msg == "no_transactions" %} +
    +
  • + {% trans 'No unpaid transactions found. Please re-check and try again.' %} +
  • +
+ {% endif %} + +

+ {% blocktrans with participation=object %} + Payments for {{ participation }} + {% endblocktrans %} +   + {% if object.is_paid %} + ({% trans 'Paid' %}) + {% else %} + ({% trans 'Not paid yet' %}) + {% endif %} +

+ +
+ + + + + + + + + + + {% for transaction in transactions %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans 'Timestamp' %}{% trans 'Description' %}{% trans 'Amount' %}{% trans 'Settled in' %}
+ {{ transaction.date|date:"d-m-Y H:i" }} + + + {{ transaction.description }} + + + € {{ transaction.price|intcomma }} + + {% include 'info/transaction_settlement_info.html' with settlement=transaction.settlement highlight_unpaid=True %} +
+ {% trans 'No transactions found. If this is a paid activity, this is probably wrong! Please contact the WWW.' %} +
+
+ + {% trans 'Return' %} + +
diff --git a/amelie/activities/urls.py b/amelie/activities/urls.py index 49bd1ab3..8745ed37 100644 --- a/amelie/activities/urls.py +++ b/amelie/activities/urls.py @@ -3,7 +3,7 @@ from amelie.activities.models import EnrollmentoptionCheckbox, EnrollmentoptionFood, EnrollmentoptionQuestion, \ EnrollmentoptionNumeric -from amelie.activities import views +from amelie.activities import views, ajax_views from amelie.activities.views import EnrollmentoptionCreateView, ActivityUpdateView, ActivityCreateView, \ EnrollmentoptionUpdateView, EnrollmentoptionDeleteView, EnrollmentoptionListView @@ -26,8 +26,8 @@ urlpatterns = [ # URLs that are not about a specific activity - path('', views.activities, name='activities'), - path('old/', views.activities_old, name='activities_old'), + path('', views.ActivityListView.as_view(), name='activities'), + path('old/', views.OldActivityListView.as_view(), name='activities_old'), # URLs about the photo gallery path('photos/', views.photos, name='photos'), path('photos//', views.photos, name='photos'), @@ -38,7 +38,10 @@ path('photos/upload/clear/', views.ClearPhotoUploadDirView.as_view(), name='photo_upload_clear'), # URLs about an activity - path('/', views.activity, name='activity'), + path('/', views.ActivityDetailView.as_view(), name='activity'), + path('/ajax/participants/', ajax_views.ActivityParticipationsView.as_view(), name='activity_participations'), + path('/ajax/participants//payments/', ajax_views.ActivityParticipationPaymentsView.as_view(), name='activity_participation_payments'), + path('/ajax/participants//payment_create/', ajax_views.ActivityParticipationCreatePaymentView.as_view(), name='activity_participation_create_payment'), path('/mailing/', views.activity_mailing, name='mailing'), path('/photos/random/', views.activity_random_photo, name='random_photo'), path('/photo//', views.gallery_photo, name='gallery_photo'), @@ -49,19 +52,18 @@ path('/edit/', ActivityUpdateView.as_view(), name='edit'), path('new/', ActivityCreateView.as_view(), name='new'), - path('/cancel/', views.activity_cancel, name='cancel'), - path('/delete/', views.activity_delete, name='delete'), + path('/cancel/', views.ActivityCancelView.as_view(), name='cancel'), + path('/delete/', views.ActivityDeleteView.as_view(), name='delete'), # URLs about enrollments - path('/enrollment/', views.activity_enrollment, name='enrollment'), - path('/enrollment_export/', views.DataExport.as_view(), name='enrollment_data_export'), - path('/enrollmentform/', views.activity_enrollment_self, name='enrollment_self'), - path('/enrollmentform/search/', views.activity_enrollment_person_search, name='enrollment_person_search'), - path('/enrollmentform//', views.activity_enrollment_person, name='enrollment_person'), - path('/unenrollment//', views.activity_unenrollment, name='unenrollment'), - path('/unenrollment/', views.activity_unenrollment_self, name='unenrollment_self'), - path('/edit_enrollment/', views.activity_editenrollment_self, name='editenrollment_self'), - path('/edit_enrollment//', views.activity_editenrollment_other, name='editenrollment_other'), + path('/enrollment/', views.ActivityEnrollView.as_view(), name='enrollment'), + path('/enrollment//', views.ActivityEnrollView.as_view(), name='enrollment_person'), + path('/enrollment/search/', views.ActivityEnrollSearchPersonView.as_view(), name='enrollment_person_search'), + path('/enrollment/unenroll/', views.ActivityUnenrollView.as_view(), name='unenrollment'), + path('/enrollment//unenroll/', views.ActivityUnenrollView.as_view(), name='unenrollment_person'), + path('/enrollment/edit/', views.ActivityEditEnrollView.as_view(), name='editenrollment'), + path('/enrollment//edit/', views.ActivityEditEnrollView.as_view(), name='editenrollment_person'), + path('/enrollment/export/', views.DataExport.as_view(), name='enrollment_data_export'), # URLs about adding a new enrollmentoption to an activity path('/enrollmentoptions/checkbox/new/', EnrollmentoptionCreateView.as_view(model=EnrollmentoptionCheckbox), name='enrollmentoption_checkbox_new'), @@ -74,10 +76,10 @@ path('enrollmentoptions//edit/', EnrollmentoptionUpdateView.as_view(), name='enrollmentoption_edit'), path('enrollmentoptions//delete/', EnrollmentoptionDeleteView.as_view(), name='enrollmentoption_delete'), - # URLs about calendars, two times due to reverse-urls - path('calendar.ics', views.activities_ics, name='activities_ics'), - re_path(r'calendar_(?P(nl|en))\.ics', views.activities_ics, name='activities_ics_international'), - path('/activity.ics', views.activity_ics, name='ics'), + # URLs about calendars + path('calendar.ics', views.ActivitiesICSView.as_view(), name='activities_ics'), + re_path(r'calendar_(?P(nl|en))\.ics', views.ActivitiesICSView.as_view(), name='activities_ics_international'), + path('/activity.ics', views.ActivityICSView.as_view(), name='ics'), # URLs for the eventdesk check pages path('eventdesk/', views.EventDeskMessageList.as_view(), name='eventdesk_list'), @@ -93,5 +95,5 @@ path('/activiteit.ics', RedirectView.as_view(url='/activities/%(pk)s/activity.ics', permanent=True)), # Activity label filtering (last because it would override other URLs otherwise) - path('', views.activities, name='activities_type'), + path('', views.ActivityListView.as_view(), name='activities_type'), ] diff --git a/amelie/activities/utils.py b/amelie/activities/utils.py deleted file mode 100644 index f742ac48..00000000 --- a/amelie/activities/utils.py +++ /dev/null @@ -1,184 +0,0 @@ -import locale -import threading -from contextlib import contextmanager - -from django.contrib import messages -from django.db import transaction -from django.utils.translation import gettext as _ - -from amelie.activities.mail import activity_send_enrollmentmail -from amelie.calendar.models import Participation - - -def check_enrollment_allowed(request, activity, person=None, indirect=False, ignore_full=False, - ignore_start_enrollment=False): - """Checks whether a person can enroll for an activity - - :param request: The request object of the current request, needed to put it in django messages - :param activity: The activity for which we need to check whether the person can enroll for it - :param person: The person who wants to enroll for an activity, standard is request.person - :param indirect: Whether the enrollment is done by the person him-/herself, or done for him/her - :param ignore_full: Ignore the enrollment limit of the activity - :param ignore_start_enrollment: Ignore the enrollment period of the activity - :returns: Whether the person can enroll for the given activity - - :side_effect: Puts an elaborate description of the error in django messages - """ - - if not person: - person = request.person - indirect = False - - if not person.is_member(): - messages.error(request, - _('Enrollment for {activity} is not possible. You do not have an active membership at Inter-Actief').format( - activity=activity, - )) - - # shorted because we only want to return an error - return False - - if not activity.enrollment: - messages.error(request, - _('Unenrollment for {activity} is not possible.').format( - activity=activity, - )) - return False - - if activity.oauth_application: - messages.error(request, - _('Enrollment for {activity} is only possible on the website of this activity.').format( - activity=activity, - )) - return False - - if not activity.enrollment_open(): - if not ignore_start_enrollment: - messages.error(request, - _("The enrollment period for {activity} hasn't started yet or has already passed.").format( - activity=activity, - )) - return False - - if person in activity.participants.all(): - if indirect: - messages.error(request, - _('{person} is already enrolled for {activity}.').format( - person=person, - activity=activity, - )) - else: - messages.error(request, - _('You are already enrolled for {activity}.').format( - activity=activity, - )) - return False - - return True - - -def check_unenrollment_allowed(request, activity, person=None, indirect=False, ignore_can_unenroll=False, - ignore_unenrollment_period=False): - """Checks whether a person can unenroll for an activity - - :param request: The request object of the current request, needed to put it in django messages - :param activity: The activity for which we need to check whether the person can unenroll - :param person: The person who wants to unenroll for an activity, standard is request.person - :param indirect: Whether the unenrollment is done by the person him-/herself or for him/her - :param ignore_can_unenroll: Ignore the unenrollment prohibition of the activity - :param ignore_unenrollment_period: Ignore the unenrollment period of the activity - :returns: Whether the person can unenroll for the given activity - - :side_effect: Puts an elaborate description of the error in django messages - """ - - if not person: - person = request.person - indirect = False - - if not activity.enrollment: - messages.error(request, - _('Unenrollment for {activity} is not possible.').format( - activity=activity, - )) - return False - - if activity.oauth_application: - messages.error(request, - _('Unenrollment for {activity} is only possible on the website of this activity.') - .format( - activity=activity, - )) - return False - - try: - # The select_for_update makes sure to acquire a write lock - Participation.objects.select_for_update().get(person=person, event=activity) - except Participation.DoesNotExist: - if indirect: - messages.error(request, - _('{person} is not enrolled for {activity}.').format( - person=person, - activity=activity, - )) - else: - messages.error(request, - _('You are not enrolled for {activity}.').format( - activity=activity, - )) - return False - - if not (ignore_can_unenroll or activity.can_unenroll): - messages.error(request, - _('Unenrollment for {activity} is not possible.').format( - activity=activity, - )) - return False - - if not (ignore_unenrollment_period or activity.enrollment_open()): - messages.error(request, - _('The unenrollment term for {activity} has already passed.').format( - activity=activity, - )) - return False - - return True - - -@transaction.atomic -def update_waiting_list(activity=None, request=None, **kwargs): - if activity is None: - activity = kwargs.get('instance') - - while not activity.enrollment_full() and activity.has_waiting_participants() and not activity.waiting_list_locked: - new_participation = Participation.objects.get( - person=activity.waiting_participants.order_by('participation__added_on').first(), - event=activity) - - activity_send_enrollmentmail(new_participation, from_waiting_list=True) - new_participation.waiting_list = False - new_participation.save() - from amelie.personal_tab import transactions - transactions.add_participation(new_participation) - if request is not None and request.is_board: - messages.info(request, _(u'{person} has been promoted from the waiting list for {activity}.').format( - person=new_participation.person, activity=activity)) - - - -LOCALE_LOCK = threading.Lock() - - -@contextmanager -def setlocale(name): - """ - A Contextmanager to change the locale within the context to the given locale string. - This allows tools like strptime to parse time strings in other locales than the system locale. - e.g. "with setlocale('en_US'): ..." - """ - with LOCALE_LOCK: - saved = locale.setlocale(locale.LC_ALL) - try: - yield locale.setlocale(locale.LC_ALL, name) - finally: - locale.setlocale(locale.LC_ALL, saved) diff --git a/amelie/activities/views.py b/amelie/activities/views.py index d21ecf81..62a17a56 100644 --- a/amelie/activities/views.py +++ b/amelie/activities/views.py @@ -8,6 +8,7 @@ import re import csv from pathlib import Path +from typing import Dict, Union, Optional, Tuple from PIL import Image from django.conf import settings @@ -17,6 +18,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied, ImproperlyConfigured from django.core.paginator import EmptyPage, PageNotAnInteger +from django.forms import Form from django.urls import reverse, reverse_lazy from django.db import transaction from django.db.models import F, Q, Count, Sum @@ -28,266 +30,249 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache -from django.views.decorators.http import require_POST from django.views.generic import ListView, DetailView from django.views.generic.base import TemplateView, View from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView -from amelie.activities.forms import ActivityForm, PaymentForm, PhotoUploadForm, \ - EnrollmentoptionCheckboxAnswerForm, EnrollmentoptionCheckboxForm, EnrollmentoptionFoodAnswerForm, \ - EnrollmentoptionFoodForm, EnrollmentoptionQuestionAnswerForm, EnrollmentoptionQuestionForm, \ - EventDeskActivityMatchForm, EnrollmentoptionNumericForm, EnrollmentoptionNumericAnswerForm, PhotoFileUploadForm +from amelie.activities.forms import ActivityForm, PhotoUploadForm, PhotoFileUploadForm, \ + ActivityEnrollmentOptionsForm, EnrollmentoptionAnswerForm, ENROLLMENTOPTION_FORM_TYPES, EventDeskActivityMatchForm from amelie.activities.mail import activity_send_enrollmentmail, activity_send_on_waiting_listmail, \ - activity_send_cancellationmail, activity_send_cashrefundmail, activity_send_price_change_mail, \ - activity_send_enrollment_option_price_change_mail + activity_send_price_change_mail, activity_send_enrollment_option_price_change_mail from amelie.activities.models import Activity, DishPrice, Enrollmentoption, EnrollmentoptionCheckbox, \ - EnrollmentoptionCheckboxAnswer, EnrollmentoptionFood, EnrollmentoptionFoodAnswer, EnrollmentoptionQuestion, \ - EnrollmentoptionQuestionAnswer, EventDeskRegistrationMessage, Restaurant, EnrollmentoptionNumeric, \ - EnrollmentoptionNumericAnswer, EnrollmentoptionAnswer + EnrollmentoptionFood, EnrollmentoptionQuestion, EventDeskRegistrationMessage, Restaurant, EnrollmentoptionNumeric from amelie.activities.tasks import save_photos -from amelie.activities.utils import check_enrollment_allowed, check_unenrollment_allowed, update_waiting_list from amelie.claudia.google import GoogleSuiteAPI from amelie.files.models import Attachment from amelie.calendar.models import Participation, Event -from amelie.graphql.helpers import is_board from amelie.members.forms import PersonSearchForm -from amelie.members.models import Person, Photographer, Preference +from amelie.members.models import Person, Photographer from amelie.members.query_forms import ActivityMailingForm from amelie.personal_tab.models import ActivityTransaction from amelie.tools import amelie_messages, types from amelie.tools.const import TaskPriority -from amelie.tools.decorators import require_actief, require_lid, require_committee, require_board -from amelie.tools.forms import PeriodForm, ExportForm, PeriodKeywordForm +from amelie.tools.decorators import require_committee +from amelie.tools.forms import ExportForm, PeriodKeywordForm from amelie.tools.calendar import ical_calendar from amelie.tools.mixins import RequireActiveMemberMixin, DeleteMessageMixin, PassesTestMixin, RequireBoardMixin, \ - RequireCommitteeMixin + RequireCommitteeMixin, RequireMemberMixin from amelie.tools.paginator import RangedPaginator logger = logging.getLogger(__name__) -ENROLLMENTOPTION_ANSWER_TYPES = { - EnrollmentoptionQuestion: EnrollmentoptionQuestionAnswer, - EnrollmentoptionCheckbox: EnrollmentoptionCheckboxAnswer, - EnrollmentoptionFood: EnrollmentoptionFoodAnswer, - EnrollmentoptionNumeric: EnrollmentoptionNumericAnswer, -} - -ENROLLMENTOPTIONANSWER_FORM_TYPES = { - EnrollmentoptionQuestion: EnrollmentoptionQuestionAnswerForm, - EnrollmentoptionCheckbox: EnrollmentoptionCheckboxAnswerForm, - EnrollmentoptionFood: EnrollmentoptionFoodAnswerForm, - EnrollmentoptionNumeric: EnrollmentoptionNumericAnswerForm, -} - -ENROLLMENTOPTION_FORM_TYPES = { - EnrollmentoptionQuestion: EnrollmentoptionQuestionForm, - EnrollmentoptionCheckbox: EnrollmentoptionCheckboxForm, - EnrollmentoptionFood: EnrollmentoptionFoodForm, - EnrollmentoptionNumeric: EnrollmentoptionNumericForm, -} - - -def activities_ics(request, lang=None): - """ - ical-feed for activities. - """ - if lang is not None: - translation.activate(lang) - act = Activity.objects \ - .filter_public(request) \ - .filter(begin__gte=timezone.now() - datetime.timedelta(100)) \ - .order_by('begin') +class ActivitiesICSView(View): + """iCal-feed for activities.""" + def get(self, *args, **kwargs): + if self.kwargs.get('lang') is not None: + translation.activate(self.kwargs.get('lang')) - ical_contents = ical_calendar(_('Events of Inter-Actief'), act, - max_duration_before_split=datetime.timedelta(days=5)) + activities = Activity.objects.filter_public(self.request) \ + .filter(begin__gte=timezone.now() - datetime.timedelta(100)) \ + .order_by('begin') - return HttpResponse(ical_contents, content_type='text/calendar; charset=UTF-8') + return HttpResponse( + ical_calendar(_('Events of Inter-Actief'), activities, max_duration_before_split=datetime.timedelta(days=5)), + content_type='text/calendar; charset=UTF-8' + ) -def activity_ics(request, pk): - """ - Returns the ical for an activity. - """ - activity = get_object_or_404(Activity, pk=pk) - if not activity.public and not request.user.is_authenticated: - raise PermissionDenied +class ActivityICSView(View): + """Returns the ical for an activity.""" + def get(self, *args, **kwargs): + activity: Activity = get_object_or_404(Activity, pk=self.kwargs.get('pk')) - resp = HttpResponse(content_type='text/calendar; charset=UTF-8') - resp.write(ical_calendar(_(activity.summary), [activity, ])) + if not activity.public and not self.request.user.is_authenticated: + raise PermissionDenied - return resp + return HttpResponse( + ical_calendar(_(activity.summary), [activity, ]), + content_type='text/calendar; charset=UTF-8' + ) -def activities(request, act_type=None): +class ActivityListView(ListView): """ Gives an overview of all upcoming activities and recent past activities. """ - activities = Event.objects.filter_public(request) - - if act_type: - activities = activities.filter(Q(activity__activity_label__name_en=act_type) | Q(activity__activity_label__name_nl=act_type)) - old_activities = list(activities.filter(end__lt=timezone.now()))[-10:] - new_activities = list(activities.filter(end__gte=timezone.now())) + model = Event + template_name = "activity_list.html" + + def get_queryset(self): + qs = self.model.objects.filter_public(self.request) + act_type = self.kwargs.get('act_type') + if act_type is not None: + qs = qs.filter( + Q(activity__activity_label__name_en=act_type) | Q(activity__activity_label__name_nl=act_type) + ) + return qs + + def get_context_data(self, *args, **kwargs): + qs = self.get_queryset() + return { + 'old_activities': [a.as_leaf_class() for a in qs.filter(end__lt=timezone.now()).order_by('-begin')[:10]], + 'new_activities': [a.as_leaf_class() for a in qs.filter(end__gte=timezone.now())], + 'only_open_enrollments': 'openEnrollments' in self.request.GET + } + +class OldActivityListView(ListView): + """ + Gives an overview of past activities. + You can filter on a date and on a few keywords. + """ - old_activities = [a.as_leaf_class() for a in old_activities] - new_activities = [a.as_leaf_class() for a in new_activities] - only_open_enrollments = 'openEnrollments' in request.GET + model = Activity + template_name = "activity_old.html" - return render(request, "activity_list.html", locals()) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form = None + def _get_form(self): + if self.form is None: + self.form = PeriodKeywordForm(to_date_required=False, keywords_required=False, data=self.request.GET or None) + return self.form -def activities_old(request): - """ - Gives an overview of past activities. + def _get_params(self) -> Dict[str, Union[str, datetime.datetime]]: + form = self._get_form() + if form.is_valid(): + keywords = form.cleaned_data['keywords'] + start_date = form.cleaned_data['from_date'] + end_date = form.cleaned_data['to_date'] or timezone.now() + else: + keywords = "" + start_date = form.fields['from_date'].initial + end_date = form.fields['to_date'].initial + + # Convert dates to datetimes + return { + 'keywords': keywords, + 'from_date': timezone.make_aware(datetime.datetime.combine(start_date, datetime.time())), + 'to_date': timezone.make_aware(datetime.datetime.combine(end_date, datetime.time())) + } + + def get_queryset(self): + qs = self.model.objects.filter_public(self.request).order_by('-end') + get_params = self._get_params() + + # Filter date range + qs = qs.filter(end__range=(get_params.get('from_date'), get_params.get('to_date'))) + + # Filter keywords + keywords = get_params.get('keywords') + if keywords: + qs = qs.filter( + Q(summary_en__icontains=keywords) | Q(description_en__icontains=keywords) | + Q(summary_nl__icontains=keywords) | Q(description_nl__icontains=keywords) + ) + + return qs + + def get_context_data(self, *args, **kwargs): + return { + 'form': self._get_form(), + 'old_activities': self.get_queryset() + } + +class ActivityDetailView(DetailView): + """Shows an activity.""" + model = Activity + template_name = "activity.html" + deanonymise = False - You can filter on a date and on a few keywords. - """ - form = PeriodKeywordForm(to_date_required=False, keywords_required=False, data=request.GET or None) - - if form.is_valid(): - keywords = form.cleaned_data['keywords'] - start_date = form.cleaned_data['from_date'] - end_date = form.cleaned_data['to_date'] or timezone.now() - else: - keywords = "" - start_date = form.fields['from_date'].initial - end_date = form.fields['to_date'].initial - - # Convert dates to datetimes - start_datetime = timezone.make_aware(datetime.datetime.combine(start_date, datetime.time())) - end_datetime = timezone.make_aware(datetime.datetime.combine(end_date, datetime.time())) - - # Filter for activities in tim range - old_activities = Activity.objects.filter_public(request).filter(end__range=(start_datetime, end_datetime)) \ - .order_by('-end') - - if 'keywords' in form.data.keys(): - # Filter for activities with keyword - old_activities = old_activities.filter(Q(summary_en__icontains=keywords) | - Q(description_en__icontains=keywords) | - Q(summary_nl__icontains=keywords) | - Q(description_nl__icontains=keywords)) - - return render(request, "activity_old.html", { - 'form': form, - 'old_activities': old_activities, - }) - - -def activity(request, pk, deanonymise=False): - """ - Shows an activity. - """ - activity = get_object_or_404(Activity, pk=pk) - if not activity.public and not request.user.is_authenticated: - raise PermissionDenied + def __init__(self, deanonymise=False, *args, **kwargs): + super().__init__(*args, **kwargs) + self.deanonymise = deanonymise - if activity.participants: - number_participants = activity.participants.count() - number_confirmed_participants = activity.confirmed_participants.count() - number_waiting_participants = activity.waiting_participants.count() - if request.user.is_authenticated and hasattr(request, 'person'): - can_edit = activity.can_edit(request.person) - else: - can_edit = False - - import icalendar - evt = icalendar.Event() - evt.add('dtstart', activity.begin) - evt.add('dtend', activity.end) - evt.add('summary', activity.summary) - - current_time = timezone.now() - - only_show_underage = hasattr(request, 'is_board') and request.is_board and (request.GET.get('underage') == "True") - - # Extra check to make sure that no sensitive data will be leaked - if can_edit: - restaurants = Restaurant.objects \ - .filter(enrollmentoptionfood__activity=activity, - dish__dishprice__enrollmentoptionfoodanswer__enrollmentoption__activity=activity) \ - .annotate(amount=Count('enrollmentoptionfood__activity'), - total_price=Sum('dish__dishprice__price')) \ - .distinct() - for restaurant in restaurants: - restaurant.dishes = DishPrice.objects \ - .filter(enrollmentoptionfoodanswer__enrollmentoption__activity=activity, - dish__restaurant=restaurant, enrollmentoptionfoodanswer__enrollment__waiting_list=False) \ - .annotate(amount=Count('enrollmentoptionfoodanswer', - filter=Q(enrollmentoptionfoodanswer__enrollment__waiting_list=False)), - total_price=Sum('price', - filter=Q(enrollmentoptionfoodanswer__enrollment__waiting_list=False))) - total_price = sum([(restaurant.dishes.aggregate(Sum('total_price')).get( - 'total_price__sum')) if restaurant.dishes.aggregate(Sum('total_price')).get( - 'total_price__sum') is not None else 0 for restaurant in restaurants]) - total_amount = sum( - [restaurant.dishes.aggregate(Sum('amount')).get('amount__sum') for restaurant in restaurants if restaurant is not None]) - - can_view_photos = activity.photos.filter_public(request).count() > 0 - obj = activity # Template laziness - - # Sorted set of participations that are used to show the enrollments - participation_set = activity.participation_set.order_by('added_on') - - - - - if only_show_underage: - confirmed_participation_set = [x for x in - activity.participation_set.filter(waiting_list=False).order_by('added_on') if - x.person.age(at=activity.begin) < 18] - confirmed_participation_set_turns_18_during_event = [x for x in confirmed_participation_set if - x.person.age(at=activity.end) >= 18] - else: - confirmed_participation_set = activity.participation_set.filter(waiting_list=False).order_by('added_on') - - public_participation_set = confirmed_participation_set.filter( - person__preferences__name="public_enrollment" - ).order_by("person__first_name") - anonymous_count = len(confirmed_participation_set) - len(public_participation_set) - - person_enrollment_public = hasattr(request, 'person') and request.person.has_preference(name="public_enrollment") - - waiting_participation_set = activity.participation_set.filter(waiting_list=True).order_by('added_on') - - if hasattr(request, 'person') and waiting_participation_set.filter(person=request.person).exists(): - added_on = waiting_participation_set.get(person=request.person).added_on - number_on_waiting_list = waiting_participation_set.filter(added_on__lte=added_on).count() - - if hasattr(request, 'person') and (request.person in obj.participants.all()): - participation = Participation.objects.get(person=request.person, event=activity) - - has_enrollment_options = activity.has_enrollmentoptions() - - # Data Export form for GDPR compliance. - export_form = ExportForm() - export_form.fields['export_details'].initial = str(obj) - - # Enable opengraph on this page - metadata_enable_opengraph = True - is_roomduty = hasattr(request, 'person') and request.person.is_room_duty() - is_committee = hasattr(request, 'person') and obj.organizer in request.person.current_committees().all() - return render(request, "activity.html", locals()) + def get_context_data(self, **kwargs): + if not self.object.public and not self.request.user.is_authenticated: + raise PermissionDenied + request_person: Optional[Person] = self.request.person if hasattr(self.request, 'person') else None -def activity_random_photo(request, pk, *args, **kwargs): - """ Returns a random photo of a specific activity. """ - act = get_object_or_404(Activity, pk=pk) - photo = act.random_photo(only_public=not hasattr(request, 'person')) + context = super().get_context_data(**kwargs) + context['obj'] = self.object # Template laziness + if self.object.participants: + context['number_participants'] = self.object.participants.count() + context['number_confirmed_participants'] = self.object.confirmed_participants.count() + context['number_waiting_participants'] = self.object.waiting_participants.count() + if self.request.user.is_authenticated and request_person: + context['can_edit'] = self.object.can_edit(request_person) + else: + context['can_edit'] = False + context['current_time'] = timezone.now() # For determining whether to show the Cancel button. + context['only_show_underage'] = hasattr(self.request, 'is_board') and self.request.is_board and (self.request.GET.get('underage') == "True") + + # Extra check to make sure that no sensitive data will be leaked + if context['can_edit']: + context['restaurants'] = Restaurant.objects.filter( + enrollmentoptionfood__activity=self.object, + dish__dishprice__enrollmentoptionfoodanswer__enrollmentoption__activity=self.object + ).annotate( + amount=Count('enrollmentoptionfood__activity'), total_price=Sum('dish__dishprice__price') + ).distinct() + # Add dishes to restaurant data + for restaurant in context['restaurants']: + restaurant.dishes = DishPrice.objects.filter( + enrollmentoptionfoodanswer__enrollmentoption__activity=self.object, + dish__restaurant=restaurant, enrollmentoptionfoodanswer__enrollment__waiting_list=False + ).annotate( + amount=Count('enrollmentoptionfoodanswer', filter=Q(enrollmentoptionfoodanswer__enrollment__waiting_list=False)), + total_price=Sum('price', filter=Q(enrollmentoptionfoodanswer__enrollment__waiting_list=False)) + ) - # Generate a JSON response - if photo: - return photo.to_http_response(format='medium', response='json') + context['total_price'] = sum( + restaurant.dishes.aggregate(Sum('total_price')).get('total_price__sum', 0) + for restaurant in context['restaurants'] + ) + context['total_amount'] = sum( + restaurant.dishes.aggregate(Sum('amount')).get('amount__sum', 0) + for restaurant in context['restaurants'] + ) + + # Sorted set of participations that are used to show the enrollments + context['participation_set'] = self.object.participation_set.order_by('added_on') + + context['confirmed_participation_set'] = self.object.participation_set.filter(waiting_list=False).order_by('added_on') + context['public_participation_set'] = context['confirmed_participation_set'].filter(person__preferences__name="public_enrollment").order_by("person__first_name") + context['anonymous_count'] = context['confirmed_participation_set'].count() - context['public_participation_set'].count() + if context['only_show_underage']: + context['confirmed_participation_set'] = [ + x for x in self.object.participation_set.filter(waiting_list=False).order_by('added_on') + if x.person.age(at=self.object.begin) < 18 + ] + context['confirmed_participation_set_turns_18_during_event'] = [ + x for x in context['confirmed_participation_set'] + if x.person.age(at=self.object.end) >= 18 + ] + + context['person_enrollment_public'] = request_person and request_person.has_preference(name="public_enrollment") + context['waiting_participation_set'] = self.object.participation_set.filter(waiting_list=True).order_by('added_on') + + if request_person and context['waiting_participation_set'].filter(person=request_person).exists(): + # Spot is equal to the count of people that were earlier or at the same point in the waiting list than the person. + added_on = context['waiting_participation_set'].get(person=request_person).added_on + context['number_on_waiting_list'] = context['waiting_participation_set'].filter(added_on__lte=added_on).count() + + if request_person and self.object.participation_set.filter(person=request_person).exists(): + context['participation'] = self.object.participation_set.get(person=request_person) + + context['has_enrollment_options'] = self.object.has_enrollmentoptions + + # Data Export form for GDPR compliance. + context['export_form'] = ExportForm() + context['export_form'].fields['export_details'].initial = str(self.object) + + # Enable opengraph on this page + context['metadata_enable_opengraph'] = True + context['is_roomduty'] = request_person and request_person.is_room_duty() + context['is_committee'] = request_person and self.object.organizer in request_person.current_committees().all() + context['deanonymise'] = self.deanonymise - # No photo - raise Http404(_('No photo')) + return context -@require_board -@transaction.atomic -def activity_delete(request, pk): +class ActivityDeleteView(RequireBoardMixin, DeleteView): """ Deletes an activity. @@ -295,504 +280,456 @@ def activity_delete(request, pk): Asks for a confirmation. """ - obj = get_object_or_404(Activity, pk=pk) - if not obj.can_edit(request.person): - raise PermissionDenied - - if request.POST: - if 'yes' in request.POST: - # Undo transactions and remove participations - from amelie.personal_tab import transactions - for participation in obj.participation_set.all(): - transactions.participation_transaction(participation, "Cancelled event", cancel=True, - added_by=request.person) - # Remove the participation - participation.delete() - - obj.delete() - return redirect(reverse('activities:activities')) - elif 'no' in request.POST: - return redirect(obj) + model = Activity + template_name = "activity_confirm_delete.html" + success_url = reverse_lazy('activities:activities') - return render(request, "activity_confirm_delete.html", locals()) + @transaction.atomic + def form_valid(self, request, *args, **kwargs): + """ + Make sure everyone is unenrolled from the activity, delete it, and then redirect to the success URL. + """ + self.object: Activity = self.get_object() + success_url = self.get_success_url() + # Make sure everyone is unenrolled from the activity + self.object.unenroll_all_participants(actor=self.request.person if hasattr(self.request, 'person') else None) + self.object.delete() + return HttpResponseRedirect(success_url) -@require_board # Cancelling an activity is only allowed by the board due to possible transactions that should be undone. -@transaction.atomic -def activity_cancel(request, pk): +class ActivityCancelView(RequireBoardMixin, DeleteView): """ Cancels or re-publishes an activity (depending on the current status of this event). + Note: Even though this view extends DeleteView, we override the Delete method so the Activity will not be deleted. Events may only be canceled if they have not yet started. + Cancelling an activity is only allowed by the board due to possible transactions that should be undone. In the case of cancellation, this function removes all enrollments (and closes the option to enroll) and undoes the corresponding transactions. - In the case of re-publishing, the event name will be reverted to its original name and enrollments will be re-opened. + In the case of re-publishing, the cancellation will be removed, but enrollments will not be re-opened. Asks for a confirmation. """ - obj = get_object_or_404(Activity, pk=pk) - - # If the event is already canceled, immediately re-publish it without any confirmation screen. - if obj.cancelled: - obj.cancelled = False - obj.save() - messages.success(request, _(f"{obj} has been successfully re-opened. Please note that any previous enrollments are not added again and that enrollments will have to be manually re-opened.")) - return redirect(obj) - - if request.POST: - if 'yes' in request.POST: - - if obj.begin < timezone.now(): - messages.error(_(f'We were unable to cancel {obj}, since it has already started.')) - return redirect(obj) - else: - # We need to explicitly set the waiting list to false because they are also included in this set. - activity_send_cancellationmail(obj.participation_set.filter(waiting_list=False), obj) - if obj.waiting_participations.exists(): - activity_send_cancellationmail(obj.waiting_participations.all(), obj, from_waiting_list=True) - - # Send an e-mail to the treasurer if people have paid in cash. - # This email contains the names and amount paid by each person. - if obj.participation_set.filter(payment_method=Participation.PaymentMethodChoices.CASH, cash_payment_made=True).exists(): - activity_send_cashrefundmail(obj.participation_set.filter(payment_method=Participation.PaymentMethodChoices.CASH, cash_payment_made=True).all(), obj, request) - - # Undo transactions and remove participations - from amelie.personal_tab import transactions - for participation in obj.participation_set.all(): - if participation.payment_method == Participation.PaymentMethodChoices.AUTHORIZATION: - transactions.participation_transaction(participation, f"Cancelled {obj}", cancel=True, added_by=request.person) - # Remove the participation - participation.delete() - - obj.cancelled = True - obj.enrollment = False - obj.save() - messages.success(request, _(f"{obj} was cancelled successfully.")) - return redirect(obj) - - return render(request, "activity_confirm_cancellation.html", locals()) + model = Activity + template_name = "activity_confirm_cancellation.html" + def get_success_url(self): + return reverse('activities:activity', kwargs={'pk': self.object.pk}) -@require_POST -@require_lid -@transaction.atomic -def activity_enrollment(request, pk): + @transaction.atomic + def form_valid(self, form): + self.object: Activity = self.get_object() + success_url = self.get_success_url() + + # If the event is already canceled, re-publish it. + if self.object.cancelled: + self.object.uncancel_activity() + messages.success( + self.request, + _("{obj} has been successfully re-opened. Please note that any previous enrollments are not added " + "again and that enrollments will have to be manually re-opened.").format(obj=self.object) + ) + return HttpResponseRedirect(success_url) + + # Else, cancel the activity. + try: + self.object.cancel_activity(actor=self.request.person if hasattr(self.request, 'person') else None) + messages.success(self.request, _("{obj} was cancelled successfully.").format(obj=self.object)) + return HttpResponseRedirect(success_url) + except ValueError: + # Error during canceling. Only occurs if the activity has already started. + messages.error( + self.request, _("We were unable to cancel {obj}, since it has already started.").format(obj=self.object) + ) + return redirect(self.object) + + +class ActivityEnrollSearchPersonView(RequireActiveMemberMixin, FormView): """ - Enrolls the currently logged in person for this activity. - - Redirect to the elaborate form if more data is needed, - or if there are any costs associated with the activity. + Search for a person to enroll for this activity. """ + template_name = "activity_enrollment_person_search.html" + form_class = PersonSearchForm - # Lock the Activity object for updating, prevent concurrent (un)enrollments - obj = get_object_or_404(Activity.objects.select_for_update(), pk=pk) - enrollment_full = obj.enrollment_full() + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.activity = None - if not check_enrollment_allowed(request, obj): - # The method check_enrollment_allowed makes sure that something is in django messages if the enrollment is not allowed. - return redirect(obj) - - # Check whether the user needs to be redirected to an enrollment form - if obj.enrollmentoption_set.count() == 0 and obj.price == 0: - # Simple enrollment: no questions or costs - participation = Participation(person=request.person, event=obj, - payment_method=Participation.PaymentMethodChoices.NONE, added_by=request.person, - waiting_list=enrollment_full) - participation.save() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['activity'] = self.activity + return context - # Send mail if preference asks for it - if request.person.has_preference(name='mail_enrollment'): - if not enrollment_full: - activity_send_enrollmentmail(participation) - messages.success(request, - _('You are now enrolled for {activity}.').format( - activity=obj, - )) - else: - activity_send_on_waiting_listmail(participation) - messages.success(request, - _('You are now on the waiting list for {activity}.').format( - activity=obj, - )) - else: - # Elaborate enrollment, redirect to the form - return redirect('activities:enrollment_self', pk=obj.pk) + def dispatch(self, request, *args, **kwargs): + self.activity: Activity = get_object_or_404(Activity, pk=self.kwargs['pk']) + if not (self.activity.can_edit(request.person) or request.person.is_room_duty()): + raise PermissionDenied + return super().dispatch(request=request, *args, **kwargs) - return redirect(obj) + def form_valid(self, form): + return redirect('activities:enrollment_person', pk=self.activity.id, person_id=form.cleaned_data['person']) -@require_POST -@require_lid -@transaction.atomic -def activity_unenrollment_self(request, pk): +class BaseActivityEnrollmentView(RequireMemberMixin, FormView): """ - Unenroll the person that is logged in for this activity. + Base view that has some shared functionality between Enrollment, Unenrollment and Edit views + to handle if someone accesses the page for themselves or for someone else (indirect as board/roomduty). """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.activity = None + self.person = None + self.indirect: bool = False - # Lock the Activity object for updating, prevent concurrent (un)enrollments - obj = get_object_or_404(Activity.objects.select_for_update(), pk=pk) + def indirect_permisison_error_msg(self): + return _("You are not allowed to access this activity page for other people.") - if not check_unenrollment_allowed(request, obj): - # If something went wrong, it will be stated in the django errors - return redirect(obj) - - # No exception can occur here, this has been checked before - participation = Participation.objects.select_for_update().get(person=request.person, event=obj) - - # If necessary, compensate the enrollment costs. - from amelie.personal_tab import transactions - transactions.remove_participation(participation) + def action_allowed_check(self) -> Tuple[bool, Optional[str]]: + return False, _("Not implemented on base enrollment view.") - # Delete the participation - participation.delete() + def dispatch_post_hook(self) -> Optional[HttpResponse]: + return None - messages.success(request, - _('You are now unenrolled for {activity}.').format( - activity=obj, - )) - - update_waiting_list(obj, request) - - # Redirect back to the activity - return redirect(obj) + @transaction.atomic + def dispatch(self, request, *args, **kwargs): + # Determine for which person the action is being taken + self.activity: Activity = get_object_or_404(Activity, pk=self.kwargs['pk']) + person_id = self.kwargs.get('person_id', None) + if person_id: + self.person: Person = get_object_or_404(Person, pk=person_id) + self.indirect = True + else: + self.person: Person = request.person + # Indirect action is only allowed by board members or roomduty members + is_roomduty = request.person.is_room_duty() + if self.indirect and not (self.activity.can_edit(request.person) or is_roomduty): + messages.error(request, self.indirect_permisison_error_msg()) + return redirect(self.activity) -@transaction.atomic -def activity_editenrollment(request, participation, obj, edited_by=None): - from amelie.personal_tab import transactions + # If action is not possible, set the reason in the django messages and return back to the activity view. + action_allowed, reason = self.action_allowed_check() + if not action_allowed: + if reason is not None: + messages.error(request, reason) + return redirect(self.activity) - # It is important to not delete the Participation, because the person's spot in the activity or on the waiting list - # could be given away to the next waiting list entry. - # So we need to update the enrollment options of the participation in-place + resp = self.dispatch_post_hook() + if resp is not None: + return resp - if not participation.waiting_list and participation.payment_method == Participation.PaymentMethodChoices.AUTHORIZATION: - # If this is a regular Participation (not waiting list and via mandate), we need to remove the old transaction from - # their personal tab to not charge them twice and to be able to re-add the updated costs later - transactions.remove_participation(participation, added_by=edited_by, is_edited_participation=True) + return super().dispatch(request, *args, **kwargs) - enrollmentoptions_forms = _build_enrollmentoptionsanswers_forms(obj, request.POST if request.POST else None, - participation.person.user) + @transaction.atomic + def post(self, *args, **kwargs): + # Locks the Activity object for updating, prevent concurrent (un)enrollments + Activity.objects.select_for_update().get(pk=self.activity.pk) + return super().post(*args, **kwargs) - # Make enrollmentoptions - for form in enrollmentoptions_forms: - if form.is_valid(): - answer = form.save(commit=False) - answer.enrollment = participation - answer.save() - form.save_m2m() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['activity'] = self.activity + context['person'] = self.person + context['indirect'] = self.indirect + return context - if not participation.waiting_list and participation.payment_method == Participation.PaymentMethodChoices.AUTHORIZATION: - # If this is a regular Participation (not waiting list and via mandate), we need to re-add - # the transaction to update the costs on the personal tab - transactions.add_participation(participation, added_by=edited_by, is_edited_participation=True) +class ActivityEnrollView(BaseActivityEnrollmentView): + """ + Enrolls a person for an activity. + If the activity is free and has no options, the enrollment is instant. + If there are costs for the enrollment, or if it has any enrollment options, a form is shown. -@require_lid -@transaction.atomic -def activity_editenrollment_self(request, pk): - """ - Edit enrollment of the person that is logged in for this activity. + The person is either the currently logged-in person or, + if the person is a board member and the `person_id` argument is given, it will enroll the selected person. """ + template_name = "activity_enrollment_form.html" + form_class = ActivityEnrollmentOptionsForm - # Lock the Activity object for updating, prevent concurrent (un)enrollments - obj = get_object_or_404(Activity.objects.select_for_update(), pk=pk) - activity = obj + def indirect_permisison_error_msg(self): + return _("You are not allowed to enroll other people for this activity.") - # No exception can occur here, this has been checked before - participation = Participation.objects.select_for_update().get(person=request.person, event=obj) + def action_allowed_check(self) -> Tuple[bool, Optional[str]]: + return self.activity.check_enrollment_allowed( + person=self.person, indirect=self.indirect, ignore_start_enrollment=self.indirect + ) - if not check_unenrollment_allowed(request, obj, ignore_can_unenroll=participation.waiting_list): - # If something went wrong, it will be stated in the django errors - return redirect(obj) + def dispatch_post_hook(self) -> Optional[HttpResponse]: + # If it's a self-enrollment for an activity with costs, and the person has no mandate, deny the enrollment. + if not self.indirect and not self.person.has_mandate_activities() and self.activity.has_costs: + return render(self.request, "activity_enrollment_mandate.html", {'activity': self.activity}) + return None - if request.method == 'POST': - activity_editenrollment(request, participation, obj, edited_by=None) - # Redirect back to the activity - return redirect(obj) - else: - enrollmentoptions_forms = _build_enrollmentoptionsanswers_forms(obj, - request.POST if request.POST else None, - request.user) - update = True - return render(request, "activity_enrollment_form.html", locals()) + def get_form_kwargs(self): + return { + 'activity': self.activity, + 'participation': None, + 'add_indirect_form': self.indirect, + 'data': self.request.POST if self.request.method == 'POST' else None, + 'initial': { + 'indirect': {'waiting_list': self.activity.enrollment_full, 'board': self.request.is_board} + } if self.indirect else {} + } + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['activity_full'] = self.activity.enrollment_full + return context -@require_committee(settings.ROOM_DUTY_ABBREVIATION) -@transaction.atomic -def activity_editenrollment_other(request, pk, person_id): - """ - Edit enrollment of the person that is logged in for this activity. - """ - # Lock the Activity object for updating, prevent concurrent (un)enrollments - obj = get_object_or_404(Activity.objects.select_for_update(), pk=pk) - person = get_object_or_404(Person, pk=person_id) - activity = obj + def send_enrollment_mail(self, participation: Participation): + # Send mail if preference asks for it, or if it is an indirect enrollment + if self.indirect or self.person.has_preference(name='mail_enrollment'): + if participation.waiting_list: + activity_send_on_waiting_listmail(participation) + else: + activity_send_enrollmentmail(participation) + def set_enrolled_status_message(self, participation: Participation): + # Set the status message in the request. + if self.indirect: + if participation.waiting_list: + msg = _('{person} is now enrolled for the waitinglist of {activity}.') + else: + msg = _('{person} is now enrolled for {activity}.') + messages.success(self.request, msg.format(person=self.person, activity=self.activity)) + else: + if participation.waiting_list: + msg = _('You are now enrolled for the waitinglist of {activity}.') + else: + msg = _('You are now enrolled for {activity}.') + messages.success(self.request, msg.format(activity=self.activity)) - try: - participation = Participation.objects.select_for_update().get(person=person, event=obj) - except Participation.DoesNotExist: - raise Http404 + def get(self, *args, **kwargs): + """ + Immediately enroll the person if it is their own enrollment, there are no options and the activity is free. + Otherwise, render the enrollment form. + """ + if not self.indirect and self.activity.enrollmentoption_set.count() == 0 and self.activity.price == 0: + # Simple enrollment: no questions or costs. Enroll the person immediately + enrollment_full = self.activity.enrollment_full + participation = Participation( + person=self.person, + event=self.activity, + added_by=self.request.person, + waiting_list=enrollment_full + ) + participation.save() - if request.method == 'POST': - activity_editenrollment(request, participation, obj, edited_by=request.person) - # Redirect back to the activity - return redirect(obj) - else: - enrollmentoptions_forms = _build_enrollmentoptionsanswers_forms(obj, - request.POST if request.POST else None, - person.user) - update = True - return render(request, "activity_enrollment_form.html", locals()) + # Maybe send a notification e-mail to the person and set a status message in the request. + self.send_enrollment_mail(participation=participation) + self.set_enrolled_status_message(participation=participation) + # Redirect back to the activity details + return redirect(self.activity) + else: + return super().get(*args, **kwargs) -@require_committee(settings.ROOM_DUTY_ABBREVIATION) -@transaction.atomic -def activity_unenrollment(request, pk, person_id): - """ - Unenroll the given person for this activity. - """ + @transaction.atomic + def form_valid(self, form): + # Check if the enrollment was indirect and the option to skip the waiting list for this enrollment was selected. + force_skip_waiting_list = self.indirect and form.cleaned_data.get('indirect', {}).get('waiting_list', None) == "skip" + + # Enroll the person + participation = Participation( + person=self.person, + event=self.activity, + added_by=self.request.person, + waiting_list=(self.activity.enrollment_full and not force_skip_waiting_list) + ) - # Lock the Activity object for updating, prevent concurrent (un)enrollments - obj = get_object_or_404(Activity.objects.select_for_update(), pk=pk) + if self.indirect: + participation.remark = 'Indirect enrollment.' - participation = get_object_or_404(Participation.objects.select_for_update(), person_id=person_id, event=obj) - person = participation.person + participation.save() - if not check_unenrollment_allowed(request, obj, person, indirect=True, ignore_can_unenroll=True, - ignore_unenrollment_period=True): - # if something went wrong, it will be stated in the django errors - return redirect(obj) + # Save enrollment option answers + for subform_id, subform in form.forms.items(): + if isinstance(subform, EnrollmentoptionAnswerForm): + answer = subform.save(commit=False) + answer.enrollment = participation + answer.save() + subform.save_m2m() - if request.POST: - if 'yes' in request.POST: - # If necessary, compensate the enrollment costs. - from amelie.personal_tab import transactions - transactions.remove_participation(participation) + # Create personal tab transaction (if the activity is paid) + price, with_enrollmentoptions = participation.calculate_costs() - # Delete the participation + # Sanity check to disallow self-enrollment for options that cost money + if price and not self.indirect and not self.person.has_mandate_activities(): + # Remember to delete the participation we created! participation.delete() + return render(self.request, "activity_enrollment_mandate.html", {'activity': self.activity}) - update_waiting_list(obj, request) + if price and (force_skip_waiting_list or not self.activity.enrollment_full): + from amelie.personal_tab import transactions + transactions.add_participation(participation=participation, added_by=self.request.person) - messages.success(request, - _('{person} is now unenrolled for {activity}.').format( - person=participation.person, - activity=obj, - )) - # Redirect back to the activity - return redirect(obj) + # Maybe send a notification e-mail to the person and set a status message in the request. + self.send_enrollment_mail(participation=participation) + self.set_enrolled_status_message(participation=participation) - # Render the confirmation page - return render(request, "activity_confirm_unenrollment.html", locals()) + # Redirect back to the activity details + return redirect(self.activity) -@require_actief -def activity_enrollment_person_search(request, pk): - """ - Search for a person to enroll for this activity. +class ActivityUnenrollView(BaseActivityEnrollmentView): """ - activity = get_object_or_404(Activity, pk=pk) - is_roomduty = request.person.is_room_duty() - if not (activity.can_edit(request.person) or is_roomduty): - raise PermissionDenied - - form = PersonSearchForm(request.POST if request.POST else None) - if form.is_valid(): - return redirect('activities:enrollment_person', activity.id, form.cleaned_data['person']) - - # Render the template - return render(request, "activity_enrollment_person_search.html", locals()) - + Unenrolls a person for an activity. -@require_lid -def activity_enrollment_self(request, pk): + The person is either the currently logged-in person or, + if the person is a board member and the `person_id` argument is given, it will unenroll the selected person. """ - Enroll yourself for an activity with an elaborate enrollment form. - """ - activity = get_object_or_404(Activity, pk=pk) - - # Enrollment for an activity with enrollment options or a price may only be done through a special form. - # However, since enrollment options possibly have costs associated with them, the person should have a mandate. - # If the person has no mandate, then (s)he should be redirected to an explanation page. - if not request.person.has_mandate_activities() and activity.has_costs(): - return render(request, "activity_enrollment_mandate.html", locals()) + template_name = "activity_confirm_unenrollment.html" + form_class = Form - return activity_enrollment_form(request, activity) + def indirect_permisison_error_msg(self): + return _("You are not allowed to unenroll other people for this activity.") + def action_allowed_check(self) -> Tuple[bool, Optional[str]]: + return self.activity.check_unenrollment_allowed( + person=self.person, indirect=self.indirect, + ignore_can_unenroll=self.indirect, ignore_unenrollment_period=self.indirect + ) -@require_lid -def activity_enrollment_person(request, pk, person_id): - """ - Enroll someone else for an activity with an elaborate enrollment form. - """ - activity = get_object_or_404(Activity, pk=pk) - person = get_object_or_404(Person, id=person_id) + @transaction.atomic + def form_valid(self, form): + # Retrieve the person's participation object and lock it for updating to prevent concurrent (un)enrollments + participation = get_object_or_404(Participation, person=self.person, event=self.activity) + Participation.objects.select_for_update().get(pk=participation.pk) - return activity_enrollment_form(request, activity, person) + # If necessary, compensate the person for the enrollment costs. + from amelie.personal_tab import transactions + transactions.remove_participation(participation) + # Delete the participation + participation.delete() -def _build_enrollmentoptionsanswers_forms(activity_obj, data, user): - forms = [] + # Set a status message for the user so they know the action is successful. + if self.indirect: + messages.success(self.request, _('{person} is now unenrolled for {activity}.').format( + person=participation.person, activity=self.activity + )) + else: + messages.success(self.request, _('You are now unenrolled for {activity}.').format(activity=self.activity)) + + # Update the waitlist, and if the unenrollment was indirect, show if anyone new was promoted from the waitlist. + new_participants = self.activity.update_waiting_list() + if self.indirect: + for np in new_participants: + messages.info( + self.request, _('{person} has been promoted from the waiting list for {activity}.').format( + person=np.person.incomplete_name(), activity=self.activity + ) + ) - for enrollmentoption in activity_obj.enrollmentoption_set.all(): - prefix = 'enrollmentoption_{}'.format(enrollmentoption.pk) + # Redirect back to the activity + return redirect(self.activity) - answer_type = ENROLLMENTOPTION_ANSWER_TYPES[enrollmentoption.content_type.model_class()] - content_type = ContentType.objects.get_for_model(answer_type) - instance = answer_type(enrollmentoption=enrollmentoption, content_type=content_type) - # Try to resolve the instance to an actual object if the person is enrolled for the activity - try: - participation = Participation.objects.get(person__user=user, event=activity_obj) - try: - instance = answer_type.objects.get( - enrollment=participation, - enrollmentoption=enrollmentoption, - content_type=content_type - ) - except EnrollmentoptionAnswer.DoesNotExist: - pass - except Participation.DoesNotExist: - pass - - form_type = ENROLLMENTOPTIONANSWER_FORM_TYPES[enrollmentoption.content_type.model_class()] - - # Make sure that if someone adjusts their enrollment, and they have a spot with a limited capacity, - # they still have the option to keep their spot. - checked = False - if instance.pk: - if form_type == EnrollmentoptionCheckboxAnswerForm: - checked = instance.answer - elif form_type == EnrollmentoptionNumericAnswerForm: - checked = instance.answer > 0 - - forms.append( - form_type(enrollmentoption=enrollmentoption, checked=checked, data=data, - prefix=prefix, instance=instance)) - return forms - - -@require_lid -@transaction.atomic -def activity_enrollment_form(request, activity, person=None): +class ActivityEditEnrollView(BaseActivityEnrollmentView): """ - Elaborate enrollment form. + Edits an enrollment of a person for an activity. - This is used to enroll for activities with costs and/or enrollment options. - or for enrolling other people. + The person is either the currently logged-in person or, + if the person is a board member and the `person_id` argument is given, it will enroll the selected person. """ + template_name = "activity_enrollment_form.html" + form_class = ActivityEnrollmentOptionsForm - # Lock the Activity object for updating, prevent concurrent (un)enrollments - Activity.objects.select_for_update().get(pk=activity.pk) - - per_mandate = False - indirect = person is not None - activity_full = activity.enrollment_full() + def indirect_permisison_error_msg(self): + return _("You are not allowed to edit enrollments of other people for this activity.") - if person is None: - person = request.person - - if not check_enrollment_allowed(request, activity, person=person, indirect=indirect, - ignore_full=request.is_board, ignore_start_enrollment=indirect): - # Django messages have been set in check_enrollment_allowed - return redirect(activity) + def action_allowed_check(self) -> Tuple[bool, Optional[str]]: + participation = Participation.objects.select_for_update().get(person=self.person, event=self.activity) + return self.activity.check_unenrollment_allowed( + person=self.person, ignore_can_unenroll=participation.waiting_list + ) - is_roomduty = request.person.is_room_duty() - if indirect and not (activity.can_edit(request.person) or is_roomduty): - raise PermissionDenied + def get_form_kwargs(self): + # Retrieve the person's participation object and lock it for updating to prevent concurrent (un)enrollments + participation = get_object_or_404(Participation, person=self.person, event=self.activity) + Participation.objects.select_for_update().get(pk=participation.pk) + return { + 'activity': self.activity, + 'participation': participation, + 'add_indirect_form': False, # Edits can't modify the waiting list placement. + 'data': self.request.POST if self.request.method == 'POST' else None, + 'initial': { + 'indirect': {'waiting_list': self.activity.enrollment_full, 'board': self.request.is_board} + } if self.indirect else {} + } - per_mandate = (request.is_board or settings.PERSONAL_TAB_COMMITTEE_CAN_AUTHORIZE) \ - and person.has_mandate_activities() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['update'] = True + return context - enrollmentoptions_forms = _build_enrollmentoptionsanswers_forms(activity, request.POST if request.POST else None, - person.user) - if request.method == 'POST': + def send_enrollment_mail(self, participation: Participation): + # Send mail if preference asks for it, or if it is an indirect enrollment + if self.indirect or self.person.has_preference(name='mail_enrollment'): + if participation.waiting_list: + activity_send_on_waiting_listmail(participation) + else: + activity_send_enrollmentmail(participation) - if indirect: - form_payment = PaymentForm(mandate=per_mandate, data=request.POST, files=request.FILES, - waiting_list=activity_full, board=request.is_board) - valid = all([form.is_valid() for form in enrollmentoptions_forms]) and form_payment.is_valid() - errors = [form.errors for form in enrollmentoptions_forms] + def set_enrolled_status_message(self, participation: Participation): + # Set the status message in the request. + if self.indirect: + if participation.waiting_list: + msg = _('{person} is now enrolled for the waitinglist of {activity}.') + else: + msg = _('{person} is now enrolled for {activity}.') + messages.success(self.request, msg.format(person=self.person, activity=self.activity)) else: - valid = all([form.is_valid() for form in enrollmentoptions_forms]) - errors = [form.errors for form in enrollmentoptions_forms] - form_payment = None - - if valid: - # Check if a participation is forced to skip waiting list - force_skip_waiting_list = False - if form_payment is not None and form_payment.cleaned_data.get('waiting_list', None) == "Skip": - force_skip_waiting_list = True - - participation = Participation(person=person, event=activity, added_by=request.person, - waiting_list=(activity_full and not force_skip_waiting_list)) - - # Place a remark if someone else does the enrollment - if indirect: - participation.remark = 'Indirect enrollment.' - participation.payment_method = form_payment.cleaned_data['method'] + if participation.waiting_list: + msg = _('You are now enrolled for the waitinglist of {activity}.') else: - participation.payment_method = Participation.PaymentMethodChoices.AUTHORIZATION + msg = _('You are now enrolled for {activity}.') + messages.success(self.request, msg.format(activity=self.activity)) - # Make participation (Deelname) - participation.save() - - # Make enrollmentoptions - for form in enrollmentoptions_forms: - answer = form.save(commit=False) + @transaction.atomic + def form_valid(self, form): + from amelie.personal_tab import transactions + # It is important to NOT delete the Participation, because the person's spot in the activity + # or on the waiting list could be given away to the next waiting list entry. + # So we need to update the enrollment options of the participation in-place + + participation: Optional[Participation] = form.participation + if participation is None: + raise ValueError(_("Participation not found while editing enrollment.")) + + if not participation.waiting_list: + # If this is a regular Participation (not waiting list), we need to remove the old transaction from + # their personal tab to not charge them twice and to be able to re-add the updated costs later + transactions.remove_participation(participation, added_by=self.request.person, is_edited_participation=True) + + # Save enrollment option answers + for subform_id, subform in form.forms.items(): + if isinstance(subform, EnrollmentoptionAnswerForm): + answer = subform.save(commit=False) answer.enrollment = participation answer.save() - form.save_m2m() - - price, with_enrollmentoptions = participation.calculate_costs() - - if not price: - # No payment method when nothing needs to be paid. - participation.payment_method = Participation.PaymentMethodChoices.NONE - participation.save() - - # Make transactions if there is a mandate - if participation.payment_method == Participation.PaymentMethodChoices.AUTHORIZATION and ( - force_skip_waiting_list or not activity_full): - from amelie.personal_tab import transactions - transactions.add_participation(participation, request.person) - - # Send mail per preference. Always send email if indirect. - if indirect or person.has_preference(name='mail_enrollment'): - if activity_full: - activity_send_on_waiting_listmail(participation) - else: - activity_send_enrollmentmail(participation) - - msg_waiting_list = "" - if activity_full: - msg_waiting_list = _("the waitinglist of") - # Done - if indirect: - messages.success(request, - _('{person} is now enrolled for {waiting_list} {activity}.').format( - person=person, - activity=activity, - waiting_list=msg_waiting_list, - )) - else: - messages.success(request, - _('You are now enrolled for {waiting_list} {activity}.').format( - activity=activity, - waiting_list=msg_waiting_list - )) - return HttpResponseRedirect(activity.get_absolute_url()) - else: - for error in errors: - for key in error.keys(): - messages.error(request, error[key][0]) - else: - if indirect: - form_payment = PaymentForm(mandate=per_mandate, waiting_list=activity_full, board=request.is_board) + subform.save_m2m() + + if not participation.waiting_list: + # If this is a regular Participation (not waiting list), we need to re-add + # the transaction to update the costs on the personal tab + transactions.add_participation(participation, added_by=self.request.person, is_edited_participation=True) + + # Redirect back to the activity details + return redirect(self.activity) + - # Render the template - return render(request, "activity_enrollment_form.html", locals()) +def activity_random_photo(request, pk, *args, **kwargs): + """ Returns a random photo of a specific activity. """ + act = get_object_or_404(Activity, pk=pk) + photo = act.random_photo(only_public=not hasattr(request, 'person')) + + # Generate a JSON response + if photo: + return photo.to_http_response(format='medium', response='json') + + # No photo + raise Http404(_('No photo')) @require_committee('MediaCie') @@ -1240,9 +1177,7 @@ def form_valid(self, form): if old_obj is not None and needs_new_transactions: # Undo and create new transactions from amelie.personal_tab import transactions - # Only consider participations paid by a mandate - # (to avoid creating transactions for members without a personal tab mandate) - for participation in old_obj.participation_set.filter(payment_method=Participation.PaymentMethodChoices.AUTHORIZATION): + for participation in old_obj.participation_set.all(): # Note - reason text is max 200 chars! reason = _("Activity '{activity}' was changed (reversal of old transaction) ({change_reasons})").format( activity=str(old_obj)[:90], change_reasons=", ".join(change_reasons) @@ -1261,11 +1196,11 @@ def form_valid(self, form): ) # Can't use transactions.participation_transaction because we need to override the date. - transaction = ActivityTransaction(price=price, description=reason, - participation=participation, event=old_obj, - person=participation.person, date=new_obj.begin, - with_enrollment_options=with_enrollment_options, added_by=person) - transaction.save() + t = ActivityTransaction(price=price, description=reason, + participation=participation, event=old_obj, + person=participation.person, date=new_obj.begin, + with_enrollment_options=with_enrollment_options, added_by=person) + t.save() return response @@ -1373,9 +1308,7 @@ def form_valid(self, form): if old_obj is not None and needs_new_transactions: # Undo and create new transactions from amelie.personal_tab import transactions - # Only consider participations paid by a mandate - # (to avoid creating transactions for members without a personal tab mandate) - for participation in old_obj.activity.participation_set.filter(payment_method=Participation.PaymentMethodChoices.AUTHORIZATION): + for participation in old_obj.activity.participation_set.all(): # Note - reason text is max 200 chars! reason = _("An enrollment option for activity '{activity}' was changed (reversal of old transaction) ({change_reasons})").format( activity=str(old_obj.activity)[:80], change_reasons=", ".join(change_reasons) @@ -1394,11 +1327,11 @@ def form_valid(self, form): ) # Can't use transactions.participation_transaction because we need to override the date. - transaction = ActivityTransaction(price=price, description=reason, - participation=participation, event=old_obj.activity, - person=participation.person, date=new_obj.activity.begin, - with_enrollment_options=with_enrollment_options, added_by=person) - transaction.save() + t = ActivityTransaction(price=price, description=reason, + participation=participation, event=old_obj.activity, + person=participation.person, date=new_obj.activity.begin, + with_enrollment_options=with_enrollment_options, added_by=person) + t.save() return response @@ -1520,9 +1453,11 @@ def export_print(request, pk): @staticmethod def export_html(request, pk): """ - Returns a HTML overview of participants for this activity. + Returns an HTML overview of participants for this activity. (Regular ActivityView but deanonimised). """ - return activity(request, pk, deanonymise=True) + av = ActivityDetailView(deanonymise=True) + av.setup(request=request, pk=pk) + return av.get(request=request, pk=pk) @staticmethod def export_csv(request, pk): diff --git a/amelie/api/activitystream.py b/amelie/api/activitystream.py index 5d258534..dc3456b1 100644 --- a/amelie/api/activitystream.py +++ b/amelie/api/activitystream.py @@ -15,7 +15,6 @@ from amelie.activities.models import Activity, EnrollmentoptionQuestion, EnrollmentoptionCheckbox, EnrollmentoptionFood from amelie.activities.models import EnrollmentoptionQuestionAnswer, EnrollmentoptionCheckboxAnswer, \ EnrollmentoptionFoodAnswer -from amelie.activities.utils import update_waiting_list from amelie.api.api import api_server from amelie.api.activitystream_utils import add_detailed_properties, add_images_property, add_thumbnails_property, \ get_basic_result @@ -654,20 +653,21 @@ def activity_signup(activity_id: int, price: float, options: dict, ctx: RpcReque try: # Lock the Activity object for updating, prevent concurrent (un)enrollments - activity = Activity.objects.select_for_update().get(id=activity_id) + activity: Activity = Activity.objects.select_for_update().get(id=activity_id) except Activity.DoesNotExist as e: raise DoesNotExistError(str(e)) - if activity.oauth_application and not activity.oauth_application == authentication.get_application(): - raise SignupError("You can only subscribe to this activity on the website of this activity.") - elif not authentication.represents(): + # Check signup restrictions + if not authentication.represents(): raise SignupError("Could not determine who you are based on your login credentials.") - elif authentication.represents() in activity.participants.all(): - raise SignupError("You are already signed up for this activity") - elif not activity.enrollment: - raise SignupError("You cannot sign up for this activity, there is no subscription.") - elif not activity.enrollment_open(): - raise SignupError("You cannot sign up for this activity, the subscription is not open") + enrollment_allowed, reason = activity.check_enrollment_allowed( + person=authentication.represents(), indirect=False, ignore_start_enrollment=False, + authentication_application=authentication.get_application() + ) + if not enrollment_allowed: + if reason is None: + raise SignupError("Could not enroll for this activity.") + raise SignupError(reason) # check if all required options are present and filled in for option in activity.enrollmentoption_set.all(): @@ -680,33 +680,34 @@ def activity_signup(activity_id: int, price: float, options: dict, ctx: RpcReque raise MissingOptionError() # save attendance - attendance = Participation(event=activity, - person=authentication.represents(), - remark='Enrollment through OAuth', - payment_method=Participation.PaymentMethodChoices.NONE, - added_by=authentication.represents(), - waiting_list=activity.enrollment_full()) - attendance.save() + participation = Participation( + person=authentication.represents(), + event=activity, + added_by=authentication.represents(), + waiting_list=activity.enrollment_full, + remark='Enrollment through OAuth' + ) + participation.save() # save options for option in activity.enrollmentoption_set.all(): answer = None if option.content_type.model_class() == EnrollmentoptionQuestion: result = optionset[option.id] if option.id in optionset.keys() else '' - answer = EnrollmentoptionQuestionAnswer(answer=result, enrollment=attendance, enrollmentoption=option) + answer = EnrollmentoptionQuestionAnswer(answer=result, enrollment=participation, enrollmentoption=option) elif option.content_type.model_class() == EnrollmentoptionCheckbox: result = option.id in optionset.keys() and optionset[option.id] - answer = EnrollmentoptionCheckboxAnswer(answer=result, enrollment=attendance, enrollmentoption=option) + answer = EnrollmentoptionCheckboxAnswer(answer=result, enrollment=participation, enrollmentoption=option) elif option.content_type.model_class() == EnrollmentoptionFood: result = option.dishprice_set.get(id=optionset[option.id]) if option.id in optionset.keys() else None - answer = EnrollmentoptionFoodAnswer(dishprice=result, enrollment=attendance, enrollmentoption=option) + answer = EnrollmentoptionFoodAnswer(dishprice=result, enrollment=participation, enrollmentoption=option) if answer: answer.content_type = ContentType.objects.get_for_model(answer) answer.save() # check for inconsistency in app - amount, with_options = attendance.calculate_costs() + amount, with_options = participation.calculate_costs() if abs(Decimal(price) - amount) > 0.02: # allow for floating point precision bugs raise SignupError("An inconsistency was detected. Please use the website to sign up.") @@ -716,20 +717,16 @@ def activity_signup(activity_id: int, price: float, options: dict, ctx: RpcReque if not authentication.represents().has_mandate_activities(): raise SignupError("A signed mandate is required. Please ask the board for a mandate.") - # Set payment by direct debit. - attendance.payment_method = Participation.PaymentMethodChoices.AUTHORIZATION - attendance.save() - - if not activity.enrollment_full(): + if not activity.enrollment_full: # add transaction - transactions.add_participation(attendance, authentication.represents()) + transactions.add_participation(participation=participation, added_by=authentication.represents()) # Send mail if desired if authentication.represents().has_preference(name='mail_enrollment'): - if not activity.enrollment_full(): - activity_send_enrollmentmail(attendance) + if participation.waiting_list: + activity_send_on_waiting_listmail(participation) else: - activity_send_on_waiting_listmail(attendance) + activity_send_enrollmentmail(participation) return True @@ -767,33 +764,35 @@ def activity_signup_revoke(activity_id: int, ctx: RpcRequestContext = None, **kw try: # Lock the Activity object for updating, prevent concurrent (un)enrollments - activity = Activity.objects.select_for_update().get(id=activity_id) + activity: Activity = Activity.objects.select_for_update().get(id=activity_id) except Activity.DoesNotExist as e: raise DoesNotExistError(str(e)) - if activity.oauth_application and not activity.oauth_application == authentication.get_application(): - raise SignupError("You can only unsubscribe to this activity on the website of this activity.") - elif not activity.enrollment: - raise SignupError("You cannot unsubscribe for this activity, it has no subscription.") - elif not authentication.represents(): + # Check signup restrictions + if not authentication.represents(): raise SignupError("Could not determine who you are based on your login credentials.") - elif not authentication.represents() in activity.participants.all(): - raise SignupError("You were not subscribed for this activity.") - elif not activity.can_unenroll: - raise SignupError( - "You cannot revoke your attendance anymore, it is not possible to unsubscribe for this activity.") - elif not activity.enrollment_open(): - raise SignupError("You cannot revoke your attendance anymore, subscription term has expired.") - - attendance = Participation.objects.select_for_update().get(person=authentication.represents(), - event=activity) - - # If necessary, create a compensation for attendance - transactions.remove_participation(attendance) - - # Delete attendance - attendance.delete() - - update_waiting_list(activity=activity) + unenrollment_allowed, reason = activity.check_unenrollment_allowed( + person=authentication.represents(), indirect=False, + ignore_can_unenroll=False, ignore_unenrollment_period=False, + authentication_application=authentication.get_application() + ) + if not unenrollment_allowed: + if reason is None: + raise SignupError("Could not unenroll for this activity.") + raise SignupError(reason) + + participation = Participation.objects.select_for_update().get( + person=authentication.represents(), event=activity + ) + + # If necessary, compensate the person for the enrollment costs. + from amelie.personal_tab import transactions + transactions.remove_participation(participation) + + # Delete the participation + participation.delete() + + # Update the waiting list. + activity.update_waiting_list() return True diff --git a/amelie/api/activitystream_utils.py b/amelie/api/activitystream_utils.py index a721734c..8202db5e 100644 --- a/amelie/api/activitystream_utils.py +++ b/amelie/api/activitystream_utils.py @@ -36,6 +36,8 @@ def get_basic_result(activity): # Add detailed properties to a result item def add_detailed_properties(activity, authentication: Authentication, result): + from amelie.activities.models import Activity + activity: Activity # For type checking authenticated = not isinstance(authentication, AnonymousAuthentication) result["description"] = strip_markdown(activity.description) @@ -48,10 +50,10 @@ def add_detailed_properties(activity, authentication: Authentication, result): result["signupStart"] = activity.enrollment_begin.isoformat() if activity.enrollment_begin else None result["signupStop"] = activity.enrollment_end.isoformat() if activity.enrollment_end else None result["availability"] = activity.maximum if activity.maximum is not None else 0 - result["signupAvailable"] = activity.enrollment and activity.enrollment_open() \ - and not activity.enrollment_full() - result["signupWaitinglist"] = activity.enrollment_full() - result["resignAvailable"] = activity.enrollment and activity.enrollment_open() \ + result["signupAvailable"] = activity.enrollment and activity.enrollment_open \ + and not activity.enrollment_full + result["signupWaitinglist"] = activity.enrollment_full + result["resignAvailable"] = activity.enrollment and activity.enrollment_open \ and activity.can_unenroll result["signedUp"] = authentication.represents() in activity.participants.all() if authenticated else False diff --git a/amelie/api/narrowcasting.py b/amelie/api/narrowcasting.py index 378c6172..c73809d8 100644 --- a/amelie/api/narrowcasting.py +++ b/amelie/api/narrowcasting.py @@ -170,7 +170,7 @@ def get_television_promotions() -> List[Dict]: promotions = TelevisionPromotion.objects.filter(start__lte=timezone.now(), end__gte=timezone.now()) for promotion in promotions: - res_dict: Dict[str, Union[str, int, bool]] = { + res_dict: Dict[str, Union[str, int, bool, None]] = { "image": "%s%s" % (settings.MEDIA_URL, str(promotion.attachment)) } @@ -181,7 +181,7 @@ def get_television_promotions() -> List[Dict]: res_dict["signupStart"] = promotion.activity.enrollment_begin.isoformat() res_dict["signupStop"] = promotion.activity.enrollment_end.isoformat() res_dict["signupMaximum"] = promotion.activity.maximum - res_dict["signupAvailable"] = promotion.activity.places_available() + res_dict["signupAvailable"] = promotion.activity.places_available result.append(res_dict) diff --git a/amelie/api/person.py b/amelie/api/person.py index 46474a42..961cebb4 100644 --- a/amelie/api/person.py +++ b/amelie/api/person.py @@ -8,6 +8,7 @@ from amelie.api.api import api_server from amelie.api.decorators import auth_required +from amelie.members.models import Person @api_server.register_procedure(name='getUserId', auth=auth_required(), context_target='ctx') @@ -253,14 +254,28 @@ def get_person_membership(ctx: RpcRequestContext = None, **kwargs) -> Union[Dict "hasEnded": person.membership.ended if person.membership.ended else False, } - if hasattr(person.membership, "payment") and person.membership.payment is not None: - result["payment"] = { - "date": person.membership.payment.date, + # Get payment info + if person.membership.is_paid(): + payment_transaction = person.membership.payment_transaction() + if payment_transaction.settlement.settlement_type == "INSTR": + payment_method_name = "Direct debit" + payment_method_description = "Authorization via Direct Debit." + payment_date = payment_transaction.settlement.debtcollectioninstruction.batch.execution_date + elif payment_transaction.settlement.settlement_type == "MANUAL": + payment_method_name = payment_transaction.settlement.manualpaymentsettlement.payment_method.name + payment_method_description = payment_transaction.settlement.manualpaymentsettlement.payment_method.description + payment_date = payment_transaction.settlement.manualpaymentsettlement.payment_date + else: + payment_method_name = "Unknown" + payment_method_description = "-" + payment_date = None + result['payment'] = { + "date": payment_date, "method": { - "name": person.membership.payment.payment_type.name, - "description": person.membership.payment.payment_type.description, + "name": payment_method_name, + "description": payment_method_description }, - "amount": person.membership.payment.amount, + "amount": payment_transaction.price } return result diff --git a/amelie/api/test_activitystream.py b/amelie/api/test_activitystream.py index 86bc538f..8ece320d 100644 --- a/amelie/api/test_activitystream.py +++ b/amelie/api/test_activitystream.py @@ -104,10 +104,10 @@ def _activity_data_detailed(activity, authenticated=False, signedup=False): "signupStart": activity.enrollment_begin.isoformat() if activity.enrollment_begin else None, "signupStop": activity.enrollment_end.isoformat() if activity.enrollment_end else None, "signedUp": signedup, - "resignAvailable": activity.enrollment and activity.enrollment_open() and activity.can_unenroll, - "signupAvailable": activity.enrollment and activity.enrollment_open() - and not activity.enrollment_full(), - "signupWaitinglist": activity.enrollment_full(), + "resignAvailable": activity.enrollment and activity.enrollment_open and activity.can_unenroll, + "signupAvailable": activity.enrollment and activity.enrollment_open + and not activity.enrollment_full, + "signupWaitinglist": activity.enrollment_full, "price": activity.price, } diff --git a/amelie/blame/registrations.py b/amelie/blame/registrations.py index 9de50726..11e3c451 100644 --- a/amelie/blame/registrations.py +++ b/amelie/blame/registrations.py @@ -8,13 +8,13 @@ from amelie.gmm.models import GMM from amelie.files.models import GMMDocument, File from amelie.members.models import Person, Committee, Faculty, Department, Study, Dogroup, \ - DogroupGeneration, Association, PaymentType, CommitteeCategory, Function, Payment + DogroupGeneration, Association, CommitteeCategory, Function from amelie.news.models import NewsItem from amelie.narrowcasting.models import TelevisionPromotion from amelie.education.models import Category, Page, EducationEvent, Complaint, ComplaintComment from amelie.about.models import Page as AboutPage -from amelie.personal_tab.models import DiscountPeriod, CustomTransaction, LedgerAccount, Article, \ - Category as CookieCornerCategory, AuthorizationType, Authorization, Amendment, PrintLogEntry +from amelie.personal_tab.models import DiscountPeriod, CustomTransaction, ManualPaymentSettlement, LedgerAccount, \ + Article, Category as CookieCornerCategory, AuthorizationType, Authorization, Amendment, PrintLogEntry, PaymentMethod from amelie.weekmail.models import WeekMail """ @@ -49,7 +49,6 @@ auditlog.register(Dogroup) auditlog.register(DogroupGeneration) auditlog.register(Association) -auditlog.register(PaymentType) auditlog.register(Person, include_fields=["first_name", "last_name_prefix", "last_name", "initials", "slug", "picture", "date_of_birth", "email_address", "account_name", "shell", "webmaster", "user"]) @@ -77,13 +76,14 @@ # Personal Tab hooks auditlog.register(DiscountPeriod) auditlog.register(CustomTransaction) +auditlog.register(ManualPaymentSettlement) auditlog.register(LedgerAccount) auditlog.register(Article) auditlog.register(CookieCornerCategory) auditlog.register(AuthorizationType) auditlog.register(Authorization) auditlog.register(Amendment) -auditlog.register(Payment) +auditlog.register(PaymentMethod) auditlog.register(PrintLogEntry) # Week mailing hooks diff --git a/amelie/calendar/admin.py b/amelie/calendar/admin.py index c6c34a18..02496ac8 100644 --- a/amelie/calendar/admin.py +++ b/amelie/calendar/admin.py @@ -12,10 +12,9 @@ class EventAdmin(admin.ModelAdmin): class ParticipationAdmin(admin.ModelAdmin): - list_display = ('id', 'event', 'person', 'payment_method', 'added_on', 'added_by',) + list_display = ('id', 'event', 'person', 'added_on', 'added_by',) search_fields = ('event__summary_nl', 'event__summary_en', 'person__first_name', 'person__last_name',) date_hierarchy = 'added_on' - list_filter = ('payment_method',) raw_id_fields = ('event', 'person', 'added_by', ) diff --git a/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py b/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py new file mode 100644 index 00000000..9109cf3b --- /dev/null +++ b/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py @@ -0,0 +1,218 @@ +# Written by Kevin Alberts on 2026-07-21 + +import logging +from decimal import Decimal + +from django.db import migrations +from django.utils import translation +from django.utils.translation import gettext as _ + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# This is a huge pain in the butt, because it's in a migration +# so we can't use any of the helper functions on the models... + +# Based on Participation.calculate_costs method (and underlying methods) +def calculate_costs(apps, participation): + # List of extra costs per enrollment option + prices_extra = [] + for enrollment_option_answer in participation.enrollmentoptionanswer_set.all(): + price_extra = Decimal(0) + + # Checkbox answers + if hasattr(enrollment_option_answer, 'enrollmentoptioncheckboxanswer'): + enrollment_option_answer = enrollment_option_answer.enrollmentoptioncheckboxanswer + if enrollment_option_answer.answer: + price_extra = enrollment_option_answer.enrollmentoption.enrollmentoptioncheckbox.price_extra + prices_extra.append(price_extra) + continue + + # Numeric answers + if hasattr(enrollment_option_answer, 'enrollmentoptionnumericanswer'): + enrollment_option_answer = enrollment_option_answer.enrollmentoptionnumericanswer + price_extra = enrollment_option_answer.enrollmentoption.enrollmentoptionnumeric.price_extra * enrollment_option_answer.answer + prices_extra.append(price_extra) + continue + + # Selectbox answers + if hasattr(enrollment_option_answer, 'enrollmentoptionselectboxanswer'): + enrollment_option_answer = enrollment_option_answer.enrollmentoptionselectboxanswer + price_extra = enrollment_option_answer.answer.price_extra + prices_extra.append(price_extra) + continue + + # Food answers + if hasattr(enrollment_option_answer, 'enrollmentoptionfoodanswer'): + enrollment_option_answer = enrollment_option_answer.enrollmentoptionfoodanswer + if enrollment_option_answer.dishprice: + price_extra = enrollment_option_answer.dishprice.price + prices_extra.append(price_extra) + continue + + # Question answers + if hasattr(enrollment_option_answer, 'enrollmentoptionquestionanswer'): + # Questions never have extra price. + prices_extra.append(price_extra) + continue + + raise ValueError(f"Unknown enrollment option answer class: ID {enrollment_option_answer.pk} {enrollment_option_answer.__class__.__name__}") + + # Calculate and return total costs, and if there were any additional costs due to enrollment options. + total_costs = participation.event.activity.price + sum(prices_extra) + return total_costs, len(prices_extra) > 0 + + +# Based on Person.incomplete_name method +def incomplete_name(person): + first_name = person.first_name + if not person.first_name and person.initials: + first_name = person.initials + if person.last_name_prefix: + return ' '.join([first_name, person.last_name_prefix, person.last_name]) + else: + return ' '.join([first_name, person.last_name]) + + +# Based on ManualPaymentSettlement.create_settlement_for_transactions method. +def create_settlement_for_transactions(ManualPaymentSettlement, SettlementManualPaymentTransaction, + transactions, payment_method, person, description, + payment_datetime, created_by=None): + transactions = list(transactions) + if not transactions: + raise ValueError("No transactions given! Cannot create an empty manual payment") + + # Calculate the total settlement amount + total_transactions_price = sum(t.price for t in transactions) or Decimal("0.00") + + # Create a ManualPayment settlement with the given payment method. + settlement = ManualPaymentSettlement.objects.create( + payment_date=payment_datetime.date(), + payment_method=payment_method, + description=description[:140], + person=person, + amount=total_transactions_price, + created_by=created_by + ) + + # Save the settlement reference in all transactions + for transaction in transactions: + transaction.settlement = settlement + transaction.save() + + # If transactions come out to a non-zero amount, add a SettlementManualPaymentTransaction for + # the total transaction amount to cancel out the balance of those transactions + if total_transactions_price != 0: + settlement_transaction = SettlementManualPaymentTransaction( + date=payment_datetime, price=-total_transactions_price, person=person, + description=description[:200], settlement=settlement + ) + settlement_transaction.save() + # Register this manual payment as the payment transaction for the settlement + settlement.manual_payment_transaction = settlement_transaction + settlement.save() + return settlement + + +def event_summary(person, event): + language = person.preferred_language + if language == "en" and event.summary_en: + summ = event.summary_en + else: + summ = event.summary_nl + if event.cancelled: + summ = f"[CANCELLED] {summ}" + return summ + + +def forwards(apps, schema_editor): + """Convert any old cash payments for activity participation to Personal Tab ActivityTransactions.""" + Participation = apps.get_model('calendar', 'Participation') + ActivityTransaction = apps.get_model('personal_tab', 'ActivityTransaction') + ManualPaymentSettlement = apps.get_model('personal_tab', 'ManualPaymentSettlement') + SettlementManualPaymentTransaction = apps.get_model('personal_tab', 'SettlementManualPaymentTransaction') + PaymentMethod = apps.get_model('personal_tab', 'PaymentMethod') + + all_participations_count = Participation.objects.all().count() + cash_participations = Participation.objects.filter(payment_method='C').order_by('pk') + cash_participation_count = cash_participations.count() + unpaid_cash_participation_count = Participation.objects.filter(payment_method='C', cash_payment_made=False).count() + logger.info('Migrating unpaid cash payments for activity participation to personal tab ActivityTransactions...') + logger.info('Total unpaid cash: {}, total cash: {}, total participations: {}'.format( + unpaid_cash_participation_count, cash_participation_count, all_participations_count + )) + try: + cash_payment_method = PaymentMethod.objects.get(name_en='Cash') + logger.info(f'Using existing Cash payment method with PK "{cash_payment_method.pk}...') + except PaymentMethod.DoesNotExist: + # No cash payment type exists, but we need one to link the new manual payment settlement to. Create one. + cash_payment_method = PaymentMethod.objects.create( + name_en="Cash", + name_nl="Contant", + description_en="Cash payment. Money goes into the brown cashbox and the payment is written down on the brown cashbox list with the student number.", + description_nl="Contante betaling. Geld gaat in de bruine kas en de betaling wordt genoteerd op de bruine kaslijst met het studentnummer.", + visible=True, visible_activities=True, visible_memberships=True + ) + logger.info(f'No cash payment type existed. Created a new one with PK "{cash_payment_method.pk}...') + for i, participation in enumerate(cash_participations): + # Translate any description strings to the user's preferred language + with translation.override(participation.person.preferred_language): + # ActivityTransaction creation code below adapted from + # `personal_tab.transactions.participation_transaction` as it was on 2026-07-21. + + # Take the beginning date/time as the date of the transaction. + price, with_enrollment_options = calculate_costs(apps=apps, participation=participation) + logger.info(f'- [{i+1}/{cash_participation_count}] {participation.id} - {incomplete_name(participation.person)} for {participation.event.summary_en} on {participation.event.begin.date()}') + + # Create the new ActivityTransaction. + activity_transaction = ActivityTransaction.objects.create( + price=price, + description=_("Manual payment required for enrollment to {event} on {date} (migrated)").format( + event=event_summary(participation.person, participation.event), + date=participation.event.begin.date(), + ), + participation=participation, + event=participation.event, + person=participation.person, + date=participation.event.begin, + with_enrollment_options=with_enrollment_options, + added_by=participation.added_by, + ) + + if participation.cash_payment_made: + # Also create a counterbalancing settlement payment, because the original payment was already made. + create_settlement_for_transactions( + ManualPaymentSettlement, + SettlementManualPaymentTransaction, + transactions=[activity_transaction], + payment_method=cash_payment_method, + person=participation.person, + description=_("Manual payment for enrollment to {event} on {date} (migrated)").format( + event=event_summary(participation.person, participation.event), + date=participation.event.begin.date(), + ), + payment_datetime=participation.event.begin, + created_by=participation.added_by, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('calendar', '0008_alter_event_participants'), + ('personal_tab', '0014_personaltabsettlement_and_more'), + ('activities', '0013_activity_enrollment_private') + ] + + operations = [ + # Convert any old unpaid cash payments for activity participation to Personal Tab transactions. + migrations.RunPython(forwards), + migrations.RemoveField( + model_name='participation', + name='cash_payment_made', + ), + migrations.RemoveField( + model_name='participation', + name='payment_method', + ), + ] diff --git a/amelie/calendar/models.py b/amelie/calendar/models.py index d224ab40..7679de6e 100644 --- a/amelie/calendar/models.py +++ b/amelie/calendar/models.py @@ -1,5 +1,7 @@ -import itertools import uuid +from decimal import Decimal +from typing import List, Tuple, Optional + import pytz import logging @@ -7,10 +9,12 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import RegexValidator, URLValidator -from django.db import models +from django.db import models, transaction from django.db.models.signals import post_save, post_delete from django.dispatch import receiver +from django.utils import translation from django.utils.translation import get_language +from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _l from amelie.files.models import Attachment @@ -18,21 +22,14 @@ from amelie.calendar.tasks import send_participation_callback from amelie.members.models import Committee, Person from amelie.personal_tab.models import ActivityTransaction +from amelie.personal_tab.transactions import get_transaction_payment_status from amelie.tools.const import TaskPriority class Participation(models.Model): - class PaymentMethodChoices(models.TextChoices): - NONE = '-', _l('No payment') - CASH = 'C', _l('Cash at the board/committee') - AUTHORIZATION = 'M', _l('Mandate') - person = models.ForeignKey(Person, verbose_name=_l('Person'), on_delete=models.PROTECT) event = models.ForeignKey('Event', verbose_name=_l('Activities'), on_delete=models.PROTECT) remark = models.TextField(blank=True, verbose_name=_l('Remarks')) - - payment_method = models.CharField(max_length=1, choices=PaymentMethodChoices.choices, verbose_name=_l('Method of payment')) - cash_payment_made = models.BooleanField(verbose_name=_l("Cash payment made"), default=False) waiting_list = models.BooleanField(verbose_name=_l("On waiting list"), default=False) added_on = models.DateTimeField(verbose_name=_l('Added on'), auto_now_add=True, null=True, blank=True) @@ -51,10 +48,48 @@ def __str__(self): else: return '(%s -> %s)' % (self.person, _l('deleted activity')) - def has_paid(self): - return not (self.payment_method == Participation.PaymentMethodChoices.CASH and self.cash_payment_made == False) + def is_free(self) -> bool: + """ + Returns if this participation is free and thus requires no payment. + """ + return self.calculate_costs()[0] == 0 - def calculate_costs(self): + def is_paid(self): + """ + Participation is paid if it costs nothing, or if the newest ActivityTransaction that exists for it is paid. + """ + if self.is_free(): + return True + else: + # The latest (by creation time) positive ActivityTransaction for this participation is the one that should be paid. + # Other unpaid ones can exist if someone un/re-enrolled for the activity, + # but they should all be compensated by a negative ActivityTransaction, thus they can be ignored. + + # Newest (by creation date) positive (or 0) price transaction + current_transaction = self.activitytransaction_set.filter(price__gte=0).order_by('-added_on').first() + if current_transaction is not None: + return current_transaction.is_paid() + else: + # Should not happen, but make a note of it in the logs if it does, and return that the participation is not paid. + logging.getLogger(__name__).exception(f"Participation {self.pk} has no ActivityTransaction linked to it!") + return False + + def payment_details(self): + """ + Returns information on how this participation was paid, or how it might probably be paid in the future. + Used mainly on the activity details page to quickly show the payment status and method of a Participation. + + If the participation is free, it returns ('free', None). + Else, see the docstring of `personal_tab.transactions.get_transaction_payment_status`. + """ + # If the participation is free, it returns None. + if self.is_free(): + return 'free', None + + current_transaction = self.activitytransaction_set.filter(price__gte=0).order_by('-added_on').first() + return get_transaction_payment_status(transaction=current_transaction, mandate_type='activities') + + def calculate_costs(self) -> Tuple[Decimal, bool]: from amelie.activities.models import Activity # For now only Activities! @@ -63,12 +98,42 @@ def calculate_costs(self): except: raise NotImplementedError("Non-activities are not yet supported!") - prices_extra = (enrollment_option_answer.get_price_extra() for enrollment_option_answer in - self.enrollmentoptionanswer_set.all()) + # List of extra costs per enrollment option + prices_extra: List[Decimal] = [ + enrollment_option_answer.get_price_extra() for enrollment_option_answer in self.enrollmentoptionanswer_set.all() + ] + # Calculate and return total costs, and if there were any additional costs due to enrollment options. + total_costs = activity.price + sum(prices_extra) + return total_costs, len(prices_extra) > 0 - with_enrollment_option, prices_extra = _has_next(prices_extra) - - return activity.price + sum(prices_extra), with_enrollment_option + @transaction.atomic + def mark_as_paid(self, payment_method, actor: Optional[Person] = None): + """ + Mark any unpaid ActivityTransaction(s) for this participation as paid, with the given payment method. + """ + # Avoid circular import + from amelie.personal_tab.models import ManualPaymentSettlement + + # Get the ActivityTransaction(s) for this Participation that still need to be paid + ats = [t for t in self.activitytransaction_set.all() if not t.is_paid()] + if not ats: + return None + + # Override language to get the description strings in the person's preferred language + with translation.override(self.person.preferred_language): + # Create a ManualPayment settlement with the given payment method to mark the activity transactions as paid. + description = _("Payment for enrollment to {event} on {date}").format( + event=self.event.summary_en, date=self.event.begin.date() + ) + settlement = ManualPaymentSettlement.create_for_transactions( + transactions=ats, + payment_method=payment_method, + person=self.person, + settlement_description=description, + payment_description=description, + created_by=actor + ) + return settlement @receiver(post_save, sender=Participation) @@ -210,10 +275,3 @@ def description_short(self): if len(total_string) > char_limit: total_string = total_string[:char_limit] + '...' return total_string - -def _has_next(iterable): - try: - first = next(iterable) - except StopIteration: - return False, [] - return True, itertools.chain([first], iterable) diff --git a/amelie/calendar/participation_urls.py b/amelie/calendar/participation_urls.py deleted file mode 100644 index b9d372b5..00000000 --- a/amelie/calendar/participation_urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import re_path - -from amelie.calendar import views - -app_name = 'calendar' - -urlpatterns = [ - re_path(r'^(?P\d+)/pay/', views.PayParticipationView.as_view(), name='pay_participation'), -] diff --git a/amelie/calendar/views.py b/amelie/calendar/views.py deleted file mode 100644 index 408d1d90..00000000 --- a/amelie/calendar/views.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.conf import settings -from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect -from django.utils import timezone -from django.utils.translation import gettext_lazy as _l -from django.views.generic import TemplateView - -from amelie.calendar.models import Participation -from amelie.tools.mixins import RequireBoardMixin, RequireCommitteeMixin - - -class PayParticipationView(RequireCommitteeMixin, TemplateView): - template_name = "confirm_payment.html" - abbreviation = settings.ROOM_DUTY_ABBREVIATION - - def get_context_data(self, **kwargs): - context = super(PayParticipationView, self).get_context_data() - participation = get_object_or_404(Participation, pk=self.kwargs['pk']) - context['obj'] = participation - return context - - def post(self, request, *args, **kwargs): - participation = get_object_or_404(Participation, pk=self.kwargs['pk']) - if 'yes' in request.POST: - participation.cash_payment_made = True - remark = _l( - f"Payment registered on {timezone.now().strftime('%d-%m-%Y')} by {request.user.person.incomplete_name()}") - participation.remark += f" {remark}" - participation.save() - messages.success(request, _l("Cash payment registered!")) - return redirect('activities:activity', participation.event_id) - - diff --git a/amelie/data_export/exporters/amelie.py b/amelie/data_export/exporters/amelie.py index 3857aff3..43bcc9d0 100644 --- a/amelie/data_export/exporters/amelie.py +++ b/amelie/data_export/exporters/amelie.py @@ -8,7 +8,8 @@ from amelie.claudia.models import Mapping, Timeline, Event as ClaudiaEvent from amelie.data_export.exporters.exporter import DataExporter from amelie.personal_tab.models import ReversalTransaction, DebtCollectionTransaction, CustomTransaction, \ - CookieCornerTransaction, ContributionTransaction, AlexiaTransaction, ActivityTransaction + CookieCornerTransaction, ContributionTransaction, AlexiaTransaction, ActivityTransaction, \ + SettlementManualPaymentTransaction, SettlementExtraBalanceTransaction, ManualPaymentSettlement class AmelieDataExporter(DataExporter): @@ -126,10 +127,30 @@ def export_calendar(self): for enrollment in person_enrollments: enrollment_data = { 'remark': str(enrollment.remark), - 'payment_method': str(enrollment.get_payment_method_display()), 'enrollment_time': str(enrollment.added_on), 'enrollment_options': [], + 'payment': {} } + if enrollment.is_paid(): + # Payment details + payment_status, payment_details = enrollment.payment_details() + payment_method = None + if payment_status == "reversed" or (payment_status == "paid" and payment_details.settlement_type == "INSTR"): + payment_method = { + "name": "Direct debit", + "date": payment_details.batch.execution_date, + "amount": payment_details.amount + } + elif payment_status == "paid" and payment_details.settlement_type == "MANUAL": + payment_method = { + "name": payment_details.payment_method.name, + "date": payment_details.payment_date, + "amount": payment_details.amount + } + enrollment_data['payment'] = { + 'status': payment_status, + 'method': payment_method + } if hasattr(enrollment, 'event'): enrollment_data['event'] = "{} ({} - {})".format(enrollment.event.summary, enrollment.event.begin, enrollment.event.end) @@ -255,13 +276,27 @@ def export_member(self): 'membership_type': str(membership.type), 'year': membership.year, 'ended_prematurely_on': str(membership.ended) if membership.ended else None, - 'payment': None, + 'payment': {} } - if hasattr(membership, 'payment') and membership.payment: + if membership.is_paid(): + # Payment details + payment_status, payment_details = membership.payment_details() + payment_method = None + if payment_status == "reversed" or (payment_status == "paid" and payment_details.settlement_type == "INSTR"): + payment_method = { + "name": "Direct debit", + "date": payment_details.batch.execution_date, + "amount": payment_details.amount + } + elif payment_status == "paid" and payment_details.settlement_type == "MANUAL": + payment_method = { + "name": payment_details.payment_method.name, + "date": payment_details.payment_date, + "amount": payment_details.amount + } membership_data['payment'] = { - 'date': str(membership.payment.date), - 'payment_type': str(membership.payment.payment_type), - 'price': str(membership.payment.amount) + 'status': payment_status, + 'method': payment_method } person_memberships.append(membership_data) member_data['memberships'] = person_memberships @@ -376,7 +411,7 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), 'event': str(transaction.event), 'participation': str(transaction.participation), @@ -387,7 +422,7 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), 'alexia_id': transaction.transaction_id, } for transaction in AlexiaTransaction.objects.filter(person=person)], @@ -396,7 +431,7 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), 'membership': str(transaction.membership) } for transaction in ContributionTransaction.objects.filter(person=person)], @@ -405,7 +440,7 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), 'article': str(transaction.article), 'amount': str(transaction.amount), @@ -415,7 +450,7 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), } for transaction in CustomTransaction.objects.filter(person=person)], 'debt_collection_transactions': [{ @@ -423,7 +458,7 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), } for transaction in DebtCollectionTransaction.objects.filter(person=person)], 'reversal_transactions': [{ @@ -431,10 +466,26 @@ def export_personal_tab(self): 'price': str(transaction.price), 'description': str(transaction.description), 'discount': str(transaction.discount) if transaction.discount else None, - 'debt_collection': str(transaction.debt_collection) if transaction.debt_collection else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, 'added_on': str(transaction.added_on), 'reversal': str(transaction.reversal), } for transaction in ReversalTransaction.objects.filter(person=person)], + 'manual_payment_transactions': [{ + 'date': str(transaction.date), + 'price': str(transaction.price), + 'description': str(transaction.description), + 'discount': str(transaction.discount) if transaction.discount else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, + 'added_on': str(transaction.added_on), + } for transaction in SettlementManualPaymentTransaction.objects.filter(person=person)], + 'extra_balance_transactions': [{ + 'date': str(transaction.date), + 'price': str(transaction.price), + 'description': str(transaction.description), + 'discount': str(transaction.discount) if transaction.discount else None, + 'settlement': str(transaction.settlement) if transaction.settlement else None, + 'added_on': str(transaction.added_on), + } for transaction in SettlementExtraBalanceTransaction.objects.filter(person=person)], }, 'rfid_cards': [{ 'code': str(card), @@ -478,6 +529,12 @@ def export_personal_tab(self): } if hasattr(instruction, 'reversal') else None, } for instruction in authorization.instructions.all()], } for authorization in person.authorization_set.all()], + 'manual_payment_settlements': [{ + 'reference': str(manual_payment.payment_reference()), + 'description': str(manual_payment.description), + 'amount': str(manual_payment.amount), + 'payment_date': str(manual_payment.payment_date), + } for manual_payment in ManualPaymentSettlement.objects.filter(person=person)], } return personal_tab_data, [] diff --git a/amelie/members/admin.py b/amelie/members/admin.py index 0dad9916..26860dbb 100644 --- a/amelie/members/admin.py +++ b/amelie/members/admin.py @@ -1,24 +1,11 @@ from django.contrib import admin -from amelie.members.models import Department, Payment, PaymentType, Committee, CommitteeCategory, Dogroup, \ +from amelie.members.models import Department, Committee, CommitteeCategory, Dogroup, \ DogroupGeneration, Faculty, Function, Membership, MembershipType, Employee, Person, Student, Study, \ StudyPeriod, Association, Preference, PreferenceCategory, UnverifiedEnrollment from amelie.tools.admin import NameAdmin, AbbreviationAdmin, AbbreviationTypeAdmin -class PaymentAdmin(admin.ModelAdmin): - list_display = ('id', 'membership', 'payment_type', 'date', 'amount') - search_fields = ('membership__member__first_name', 'membership__member__last_name',) - list_filter = ('payment_type', 'amount', 'membership__type', 'membership__year',) - date_hierarchy = 'date' - raw_id_fields = ('membership',) - -class PaymentTypeAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'description', 'visible') - list_filter = ('visible',) - search_fields = ('name', 'description') - - class CommitteeAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'abbreviation', 'category', 'superuser',) search_fields = ('name', 'abbreviation') @@ -93,12 +80,10 @@ class StudyAdmin(admin.ModelAdmin): admin.site.register(Function, FunctionAdmin) admin.site.register(Committee, CommitteeAdmin) admin.site.register(CommitteeCategory, NameAdmin) -admin.site.register(Payment, PaymentAdmin) admin.site.register(Membership, MembershipAdmin) admin.site.register(Employee, EmployeeAdmin) admin.site.register(StudyPeriod, StudyPeriodAdmin) admin.site.register(Student, StudentAdmin) -admin.site.register(PaymentType, PaymentTypeAdmin) admin.site.register(MembershipType, MembershipTypeAdmin) admin.site.register(Person, PersonAdmin) admin.site.register(Preference, PreferenceAdmin) diff --git a/amelie/members/ajax_views.py b/amelie/members/ajax_views.py index ab38cd4b..c605c119 100644 --- a/amelie/members/ajax_views.py +++ b/amelie/members/ajax_views.py @@ -2,25 +2,30 @@ from datetime import date import re +from typing import Optional, Dict from django.conf import settings +from django.db import transaction from django.forms.models import inlineformset_factory -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.template.defaultfilters import slugify +from django.urls import reverse +from django.utils import translation +from django.utils.translation import gettext as _ from django.views.generic import FormView, TemplateView from documenso_sdk import DocumensoError from amelie.members.forms import CommitteeForm, FunctionForm, MembershipEndForm, MembershipForm, \ - MandateEndForm, MandateForm, EmployeeForm, PersonPaymentForm, PersonStudyForm, PersonPreferencesForm, \ - SaveNewFirstModelFormSet, StudentForm, SignatureRequestForm -from amelie.members.models import Payment, Committee, Function, Membership, Employee, PaymentType, Person, Student, \ + MandateEndForm, MandateForm, EmployeeForm, PersonStudyForm, PersonPreferencesForm, \ + SaveNewFirstModelFormSet, StudentForm, SignatureRequestForm, MembershipManualPaymentForm +from amelie.members.models import Committee, Function, Membership, Employee, Person, Student, \ StudyPeriod, Preference, PreferenceCategory from amelie.members.query_views import filter_member_list_public -from amelie.personal_tab.models import Authorization +from amelie.personal_tab.models import Authorization, ManualPaymentSettlement, PaymentMethod from amelie.personal_tab.pos_views import require_cookie_corner_pos from amelie.tools.decorators import require_ajax, require_board, require_actief, require_committee -from amelie.tools.mixins import RequireCommitteeMixin +from amelie.tools.mixins import RequireCommitteeMixin, RequireAjaxMixin def person_data(request, obj, type, form_type, view_template=None, edit_template=None, *args, **kwargs): @@ -118,24 +123,49 @@ def person_study_new(request, id): @require_ajax @require_committee(settings.ROOM_DUTY_ABBREVIATION) -def person_payments(request, id, membership): - obj = get_object_or_404(Person, id=id) +def person_membership_payments(request, id, membership): + """ + Shows a table of ContributionTransasctions for this membership, and their payment states. + """ + person = get_object_or_404(Person, id=id) membership = get_object_or_404(Membership, id=membership) - if request.method == "GET": - if 'original' in request.GET: - return render(request, "person_membership.html", locals()) - form = PersonPaymentForm() - return render(request, "person_payment.html", locals()) - elif request.method == "POST": - form = PersonPaymentForm(request.POST) - if form.is_valid(): - payment = Payment(membership=membership, payment_type=form.cleaned_data['method'], - amount=membership.type.price, date=form.cleaned_data["date"]) - payment.save() - return render(request, "person_membership.html", locals()) + transactions = membership.contributiontransaction_set.all() + context = { + 'person': person, + 'membership': membership, + 'transactions': transactions, + 'status_msg': request.GET.get('msg') + } + return render(request, "person_membership_payments.html", context) + + +class MembershipCreatePaymentView(RequireAjaxMixin, RequireCommitteeMixin, FormView): + template_name = "person_membership_create_payment.html" + abbreviation = settings.ROOM_DUTY_ABBREVIATION + form_class = MembershipManualPaymentForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['person'] = get_object_or_404(Person, id=self.kwargs['id']) + context['membership'] = get_object_or_404(Membership, id=self.kwargs['membership']) + context['transactions'] = [t for t in context['membership'].contributiontransaction_set.all() if not t.is_paid()] + context['transactions_total'] = sum(t.price for t in context['transactions']) + return context + + def get_success_url(self, query: Optional[Dict] = None): + return reverse('members:person_membership_payments', kwargs={ + 'id': self.kwargs['id'], 'membership': self.kwargs['membership'] + }, query=query) + + def form_valid(self, form): + # Get the transactions that need to be paid. + membership = get_object_or_404(Membership, id=self.kwargs['membership']) + payment_method = form.cleaned_data['payment_method'] + settlement = membership.mark_as_paid(payment_method=payment_method, actor=self.request.person) + if settlement: + return HttpResponseRedirect(self.get_success_url(query={'msg': 'created'})) else: - response = render(request, "person_payment.html", locals()) - return response + return HttpResponseRedirect(self.get_success_url(query={'msg': 'no_transactions'})) @require_ajax @@ -152,9 +182,12 @@ def person_membership_new(request, id): if request.method == "POST": form = MembershipForm(request.POST) if form.is_valid(): - membership = form.save(commit=False) - membership.member = obj - membership.save() + with transaction.atomic(): + # Save the membership and create a ContributionTransaction on the person's tab. + membership = form.save(commit=False) + membership.member = obj + membership.save() + membership.create_contribution_transaction() return render(request, "person_membership.html", locals()) else: response = render(request, "person_new_membership.html", locals()) @@ -167,20 +200,43 @@ def person_membership_new(request, id): @require_ajax @require_committee(settings.ROOM_DUTY_ABBREVIATION) def person_membership_end(request, id): - obj = get_object_or_404(Person, id=id) - membership = obj.membership_set.latest('year', 'pk') + obj: Person = get_object_or_404(Person, id=id) + membership: Membership = obj.membership_set.latest('year', 'pk') if request.method == "POST": form = MembershipEndForm(request.POST, instance=membership) - if form.is_valid(): - if form.cleaned_data['payment_needed'] == False and not Payment.objects.filter(membership=membership).exists(): - # If payment is not needed and payment does not exist yet, create one - # PaymentType is 10: "Voortijdige beëindiging, betaling niet noodzakelijk." - Payment.objects.create(membership=membership, payment_type=PaymentType.objects.get(pk=10), amount=membership.type.price, date=form.cleaned_data['ended']) - form.save() - return render(request, "person_membership.html", locals()) - else: - response = render(request, "person_end_membership.html", locals()) - return response + with transaction.atomic(): + if form.is_valid(): + if form.cleaned_data['payment_needed'] == False and not membership.is_paid(): + # If payment is no longer needed and payment does not exist yet, create a settlement for all open + # ContributionTransactions of this membership, with payment method "Early termination" (configured in settings) + transactions = [t for t in membership.contributiontransaction_set.all() if not t.is_paid()] + payment_method = PaymentMethod.objects.get(pk=settings.MEMBERS_EARLY_TERMINATION_PAYMENT_METHOD_ID) + + # Create a ManualPaymentSettlement with the selected payment method + if transactions: + # Translate the description to the user's preferred language + with translation.override(membership.member.preferred_language): + settlement_description = _( + "Early termination of membership {membership_type} ({begin_year}/{end_year})").format( + membership_type=membership.type.name, + begin_year=membership.year, + end_year=(membership.year + 1) + ) + ManualPaymentSettlement.create_for_transactions( + transactions=transactions, + payment_method=payment_method, + person=membership.member, + settlement_description=settlement_description, + payment_description=settlement_description, + created_by=request.person, + ) + else: + raise ValueError(f"Tried to create early termination payment for unpaid membership {membership.pk} but no transactions exist!") + form.save() + return render(request, "person_membership.html", locals()) + else: + response = render(request, "person_end_membership.html", locals()) + return response else: form = MembershipEndForm(instance=membership) return render(request, "person_end_membership.html", locals()) diff --git a/amelie/members/forms.py b/amelie/members/forms.py index 9f5bdcef..b8c4a902 100644 --- a/amelie/members/forms.py +++ b/amelie/members/forms.py @@ -22,10 +22,10 @@ from amelie.tools.const import TaskPriority from amelie.iamailer.mailtask import MailTask, Recipient -from amelie.members.models import Department, PaymentType, Committee, CommitteeCategory, DogroupGeneration, \ +from amelie.members.models import Department, Committee, CommitteeCategory, DogroupGeneration, \ Function, Membership, MembershipType, Employee, Person, Student, Study, StudyPeriod, Preference, \ LANGUAGE_CHOICES, UnverifiedEnrollment -from amelie.personal_tab.models import Authorization, AuthorizationType +from amelie.personal_tab.models import Authorization, AuthorizationType, PaymentMethod from amelie.tools.logic import current_academic_year_with_holidays, current_association_year from amelie.tools.validators import CheckDigitValidator from amelie.tools.widgets import DateSelector, MemberSelect @@ -286,11 +286,6 @@ class Meta: fields = ("study", "begin", "dogroup",) -class PersonPaymentForm(forms.Form): - date = forms.DateField(initial=datetime.date.today()) - method = forms.ModelChoiceField(PaymentType.objects.filter(visible=True)) - - class MembershipForm(forms.ModelForm): year = forms.IntegerField(initial=current_association_year, widget=widgets.RadioSelect()) type = forms.ModelChoiceField(MembershipType.objects.filter(active=True), label=_l('Type')) @@ -305,6 +300,13 @@ def __init__(self, *args, **kwargs): (current_association_year() + 1, _l('Upcoming association year')),) +class MembershipManualPaymentForm(forms.Form): + payment_method = forms.ModelChoiceField( + queryset=PaymentMethod.objects.filter(visible_memberships=True), + label=_l('Payment method') + ) + + class MembershipEndForm(forms.ModelForm): ended = forms.DateField(widget=widgets.RadioSelect) payment_needed = forms.BooleanField( @@ -324,6 +326,7 @@ def __init__(self, *args, **kwargs): (date(current_association_year() + 1, 7, 1), _l('At the end of the association year')),) + class SignatureRequestForm(forms.Form): send_check = forms.BooleanField(label=_l('Send the signature request'), required=True) diff --git a/amelie/members/management/commands/yeartransfer.py b/amelie/members/management/commands/yeartransfer.py index f6e3efdd..5d9f133f 100644 --- a/amelie/members/management/commands/yeartransfer.py +++ b/amelie/members/management/commands/yeartransfer.py @@ -1,5 +1,4 @@ import datetime -from decimal import Decimal from django.core.management.base import BaseCommand, CommandError from django.utils.encoding import smart_str @@ -7,7 +6,7 @@ from amelie.tools.const import TaskPriority from amelie.iamailer.mailtask import MailTask from amelie.members.management.commands.dataupdatemail import dataupdate_context -from amelie.members.models import Payment, PaymentType, Membership, MembershipType +from amelie.members.models import Membership, MembershipType from amelie.personal_tab.debt_collection import authorization_contribution from amelie.tools.mail import PersonRecipient @@ -47,7 +46,6 @@ def handle(self, *args, **options): except ValueError: raise CommandError("Type numbers for years...") - # These memberships are automatically prolonged without change, # unless the membership was terminated by the member yearmembershiptypes_text = [ @@ -136,9 +134,6 @@ def handle(self, *args, **options): priority=TaskPriority.LOW ) - paymenttype_continuation = PaymentType.objects.filter(name='Studielang (vervolg)')[0] - paymenttype_sponsored = PaymentType.objects.filter(name='Sponsoring')[0] - if prev_prolong: memberships = Membership.objects.filter(year=current_year, type__needs_verification=True) else: @@ -153,16 +148,12 @@ def handle(self, *args, **options): next_type = next_membership_type[membership.type] prolong = False - payment_type = None if membership.type in yearmembershiptypes: prolong = True - if next_type.price == Decimal("0.00"): - payment_type = paymenttype_sponsored elif membership.type in studylongmembershiptypes and membership.is_verified(): prolong = True - payment_type = paymenttype_continuation # Check if there actually is a new membership already, # and set prolong to True if it was not yet, so they get a prolongation mail instead of a discontinuation mail. @@ -193,23 +184,15 @@ def handle(self, *args, **options): context=context)) if commit: - l, created = Membership.objects.get_or_create(member=membership.member, type=next_type, - year=new_year) - if not created: + m, created = Membership.objects.get_or_create( + member=membership.member, type=next_type, year=new_year + ) + if created: + # Create a ContributionTransaction for the membership, if necessary, so it will be paid for. + m.create_contribution_transaction() + else: errorfile.write(smart_str(f"{membership.member} ({membership.type}) was already enrolled for the new year\n")) - if payment_type: - if created: - b = Payment.objects.create(date=datetime.datetime(new_year, 7, 1), - membership=l, - payment_type=payment_type, - amount=next_type.price) - b.save() - else: - errorfile.write(smart_str(f"{membership.member} ({membership.type}) was already enrolled for the new year, no payment was created!\n")) - - l.save() - else: if not prev_prolong and membership.type != ENIAC_MEMBERSHIP: discontinuationmails_first.add_recipient(PersonRecipient(recipient=membership.member)) diff --git a/amelie/members/members_urls.py b/amelie/members/members_urls.py index d5734360..5f7a2ecc 100644 --- a/amelie/members/members_urls.py +++ b/amelie/members/members_urls.py @@ -3,7 +3,6 @@ from django.views.generic import RedirectView from amelie.members import ajax_views, query_views, views -from amelie.members.views import PaymentDeleteView urlpatterns = [ path('', query_views.query, name='query'), @@ -11,7 +10,6 @@ path('mailing/', query_views.send_mailing, name='send_mailing'), path('push/', query_views.SendNotification.as_view(), name='send_push'), path('data_export/', query_views.DataExport.as_view(), name='data_export'), - path('statistics/payments/', views.payment_statistics, name='statistics_payments'), path('statistics/', views.statistics, name='statistics'), path('//', views.person_view, name='person_view'), path('//edit/', views.person_edit, name='person_edit'), @@ -21,8 +19,9 @@ path('/ajax/employee/', ajax_views.person_employee, name='person_employee'), path('/ajax/preferences/', ajax_views.person_preferences, name='person_preferences'), path('/ajax/study/', ajax_views.person_study, name='person_study'), - path('/ajax/membership/payments//', ajax_views.person_payments, name='person_pay'), path('/ajax/membership/', ajax_views.person_membership, name='person_membership'), + path('/ajax/membership//payments/', ajax_views.person_membership_payments, name='person_membership_payments'), + path('/ajax/membership//payment_create/', ajax_views.MembershipCreatePaymentView.as_view(), name='person_membership_create_payment'), path('/ajax/membership/new/', ajax_views.person_membership_new, name='person_membership_new'), path('/ajax/membership/end/', ajax_views.person_membership_end, name='person_membership_end'), path('/ajax/membership//', ajax_views.person_membership, name='person_membership'), @@ -45,8 +44,6 @@ path('groupinfo/', views.person_groupinfo, name='person_groupinfo'), path('send_link_code//', views.person_send_link_code, name='send_oauth_link_code'), - path('payment//delete/', PaymentDeleteView.as_view(), name='payment_delete'), - path('birthdays/', views.birthdays, name='birthdays'), path('new/general/', views.RegisterNewGeneralWizardView.as_view(), name='person_new_general'), diff --git a/amelie/members/migrations/0020_add_public_enrollment_preference.py b/amelie/members/migrations/0020_add_public_enrollment_preference.py index 90100757..b795002b 100644 --- a/amelie/members/migrations/0020_add_public_enrollment_preference.py +++ b/amelie/members/migrations/0020_add_public_enrollment_preference.py @@ -1,14 +1,13 @@ # Made by Conner v5.2.12 on 2026-05-06 08:41 from django.db import migrations -import time import logging + logger = logging.getLogger(__name__) -handler = logging.StreamHandler() -logger.addHandler(handler) logger.setLevel(logging.INFO) + def forwards(apps, schema_editor): Preference = apps.get_model("members", "Preference") pref = Preference.objects.filter(name='public_enrollment').first() diff --git a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py new file mode 100644 index 00000000..ec9bc2e7 --- /dev/null +++ b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py @@ -0,0 +1,299 @@ +# Written by Kevin Alberts on 2026-07-23 +import datetime +from datetime import timezone as tz +import logging +from decimal import Decimal + +from django.db import migrations +from django.db.models import Model +from django.utils import timezone, translation +from django.utils.translation import gettext as _ + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +# Based on Person.incomplete_name method +def incomplete_name(person): + first_name = person.first_name + if not person.first_name and person.initials: + first_name = person.initials + if person.last_name_prefix: + return ' '.join([first_name, person.last_name_prefix, person.last_name]) + else: + return ' '.join([first_name, person.last_name]) + + +# Based on ManualPaymentSettlement.create_settlement_for_transactions method. +def create_settlement_for_transactions(ManualPaymentSettlement, SettlementManualPaymentTransaction, + transactions, payment_method, person, description, + payment_datetime, created_by=None): + transactions = list(transactions) + if not transactions: + raise ValueError("No transactions given! Cannot create an empty manual payment") + + # Calculate the total settlement amount + total_transactions_price = sum(t.price for t in transactions) or Decimal("0.00") + + # Create a ManualPayment settlement with the given payment method. + settlement = ManualPaymentSettlement.objects.create( + payment_date=payment_datetime.date(), + payment_method=payment_method, + description=description[:140], + person=person, + amount=total_transactions_price, + created_by=created_by + ) + + # Save the settlement reference in all transactions + for transaction in transactions: + transaction.settlement = settlement + transaction.save() + + # If transactions come out to a non-zero amount, add a SettlementManualPaymentTransaction for + # the total transaction amount to cancel out the balance of those transactions + if total_transactions_price != 0: + settlement_transaction = SettlementManualPaymentTransaction( + date=payment_datetime, price=-total_transactions_price, person=person, + description=description[:200], settlement=settlement + ) + settlement_transaction.save() + # Register this manual payment as the payment transaction for the settlement + settlement.manual_payment_transaction = settlement_transaction + settlement.save() + return settlement + + +def membership_type_name(person, membership_type): + language = person.preferred_language + if language == "en" and membership_type.name_en: + return membership_type.name_en + else: + return membership_type.name_nl + + +def forwards(apps, schema_editor): + """ + Create ContributionTransactions for all memberships that do not have one, + and add a Manual Payments for all memberships that have a Payment that is not direct debit. + """ + Membership = apps.get_model('members', 'Membership') + PaymentType = apps.get_model('members', 'PaymentType') + ManualPaymentSettlement = apps.get_model('personal_tab', 'ManualPaymentSettlement') + ContributionTransaction = apps.get_model('personal_tab', 'ContributionTransaction') + ReversalTransaction = apps.get_model('personal_tab', 'ReversalTransaction') + SettlementManualPaymentTransaction = apps.get_model('personal_tab', 'SettlementManualPaymentTransaction') + PaymentMethod = apps.get_model('personal_tab', 'PaymentMethod') + logger.info(f'Creating ContributionTransactions and Manual Payments for any unpaid memberships and memberships that were not paid by Authorization...') + try: + internal_settlement_payment_method = PaymentMethod.objects.get(name_en='Internal settlement') + logger.info(f'Using existing Internal settlement payment method with PK "{internal_settlement_payment_method.pk}"...') + except PaymentMethod.DoesNotExist: + # No Internal settlement payment type exists, but we need one to link any settlements for reversed contributions to. Create one. + internal_settlement_payment_method = PaymentMethod.objects.create( + name_en="Internal settlement", + name_nl="Interne verrekening", + description_en="Internal settlement. These transactions cancel each other out to a zero balance.", + description_nl="Interne verrekening. Deze transacties zijn met elkaar verrekend tot een nulbalans.", + visible=True, visible_memberships=False, visible_activities=False + ) + logger.info(f'No Internal settlement payment type existed. Created a new one with PK "{internal_settlement_payment_method.pk}"...') + try: + pre_sepa_payment_method = PaymentMethod.objects.get(name_en='Pre-SEPA') + logger.info(f'Using existing Pre-SEPA payment method with PK "{pre_sepa_payment_method.pk}"...') + except PaymentMethod.DoesNotExist: + # No Pre-SEPA payment type exists, but we need one to link the new manual payment settlement to. Create one. + pre_sepa_payment_method = PaymentMethod.objects.create( + name_en="Pre-SEPA", + name_nl="Pre-SEPA", + description_en="Pre-SEPA payment. This payment was made before the introduction of SEPA authorizations.", + description_nl="Pre-SEPA-betaling. Deze betaling is gedaan voor de introductie van SEPA-machtigingen.", + visible=False, visible_memberships=False, visible_activities=False + ) + logger.info(f'No Pre-SEPA payment type existed. Created a new one with PK "{pre_sepa_payment_method.pk}"...') + + # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET + pre_sepa_begin_date = datetime.date(2013, 10, 31) + pre_sepa_transaction_date = datetime.datetime(2013, 10, 30, 22, 59, 59, tzinfo=tz.utc) + + # Create ContributionTransactions for all memberships that do not have one but need one, + # and create a Manual Payments for the memberships that have been paid. + membership_count = Membership.objects.count() + num_cts_created = 0 + num_settlements_created = 0 + num_left_unpaid = 0 + created_internal_settlements = 0 + try: + authorization_payment_type = PaymentType.objects.get(pk=4) # Annual authorization + except PaymentType.DoesNotExist: + authorization_payment_type = None + for i, membership in enumerate(Membership.objects.all().order_by('pk')): + # Translate any description strings to the user's preferred language + with translation.override(membership.member.preferred_language): + # If this concerns a paid membership (free memberships don't need any transactions) + if membership.type.price != 0: + if i % 1000 == 0: + logger.info(f'- [{i}/{membership_count}] Processing ID {membership.pk}...') + # ContributionTransactions need to be created for memberships that: + if ( + # - Are not paid and currently have no positive ContributionTransaction without a settlement + (not hasattr(membership, 'payment') and not membership.contributiontransaction_set.filter(price__gt=0, settlement=None).exists()) + or + # - Are paid with a payment type that is not Authorization (manual payments) + (hasattr(membership, 'payment') and membership.payment.payment_type != authorization_payment_type) + or + # - Are paid with an Authorization, but has no positive ContributionTransaction + # (if pre-sepa: that's ok, it will be marked paid later, if post-sepa: manually created authorization payment, wrong, will be left unpaid) + (hasattr(membership, 'payment') and membership.payment.payment_type == authorization_payment_type and not membership.contributiontransaction_set.filter(price__gt=0).exists()) + ): + # If this membership is paid, use the payment date as the date for the transaction, else, just guesstimate. + if hasattr(membership, 'payment') and membership.payment.date: + date = datetime.datetime.combine( + membership.payment.date, datetime.time(0, 0) + ).replace(tzinfo=timezone.get_default_timezone()) + else: + # 1st of September in the year of the membership. + date = datetime.datetime(membership.year, 9, 1, 0, 0, 0, tzinfo=tz.utc) + + # Create the contributiontransaction + ct = ContributionTransaction( + date=date, price=membership.type.price, person=membership.member, + membership=membership, settlement=None, + description=_("Contribution {membership_type} ({begin_year}/{end_year})").format( + membership_type=membership_type_name(membership.member, membership.type), + begin_year=membership.year, + end_year=(membership.year + 1) + ) + ) + ct.save() + num_cts_created += 1 + + # Negative ContributionTransactions should cancel out against a positive ReversalTransaction on the same date. + # If any unpaid negative transactions exist, find their corresponding Reversal and make a settlement so they are marked as paid. + unpaid_negative_transactions = membership.contributiontransaction_set.filter(price__lt=0, settlement=None) + for unpaid_negative_transaction in unpaid_negative_transactions: + # Try to find the corresponding unpaid ReversalTransaction + try: + reversal = ReversalTransaction.objects.get( + date=unpaid_negative_transaction.date, + price=-unpaid_negative_transaction.price, + person=unpaid_negative_transaction.person, + settlement=None + ) + except ReversalTransaction.DoesNotExist: + # Give up, either the reversal was already settled against something else or this is a different + # situation all together. The negative transaction will remain unpaid in the personal tab. + logger.info(f' - Membership {membership.pk} - Cannot find reversal for transaction {unpaid_negative_transaction.pk} ({unpaid_negative_transaction.price}) "{unpaid_negative_transaction.description}".') + continue + except ReversalTransaction.MultipleObjectsReturned: + # Give up, somehow there are multiple reversals on the exact same datetime. + # The negative transaction will remain unpaid in the personal tab. + logger.info(f' - Membership {membership.pk} - Found multiple reversals for transaction {unpaid_negative_transaction.pk} ({unpaid_negative_transaction.price}) "{unpaid_negative_transaction.description}".') + continue + + # Create a settlement that will cancel out the two transactions + logger.info(f' - Membership {membership.pk} - Cancelling transaction {unpaid_negative_transaction.pk} ({unpaid_negative_transaction.price}) against {reversal.pk} ({reversal.price}).') + create_settlement_for_transactions( + ManualPaymentSettlement, + SettlementManualPaymentTransaction, + transactions=[unpaid_negative_transaction, reversal], + payment_method=internal_settlement_payment_method, + person=membership.member, + description=_("Internal settlement for reversed contribution {membership_type} ({begin_year}/{end_year}) (migrated)").format( + membership_type=membership_type_name(membership.member, membership.type), + begin_year=membership.year, end_year=(membership.year + 1) + ), + payment_datetime=unpaid_negative_transaction.date, + created_by=None + ) + created_internal_settlements += 1 + + # If a payment exists for this membership, add any unpaid positive transactions to a ManualPayment. + # (the negative transactions cross off against positive ReversalTransactions that are not linked to the contribution) + if hasattr(membership, 'payment') and membership.payment.payment_type != authorization_payment_type: + unpaid_transactions = membership.contributiontransaction_set.filter(price__gt=0, settlement=None) + if unpaid_transactions.exists(): + # Must exist due to being created in migration personal_tab.0014_personaltabsettlement_and_more + payment_method = PaymentMethod.objects.get(name_en=membership.payment.payment_type.name) + if membership.payment.date: + transaction_date = datetime.datetime.combine( + membership.payment.date, datetime.time(0, 0) + ).replace(tzinfo=timezone.get_default_timezone()) + else: + # 1st of September in the year of the membership. + transaction_date = datetime.datetime( + membership.year, 9, 1, 0, 0, 0, tzinfo=tz.utc + ) + # Create a ManualPayment settlement with the same payment method as the old Payment. + create_settlement_for_transactions( + ManualPaymentSettlement, + SettlementManualPaymentTransaction, + transactions=unpaid_transactions, + payment_method=payment_method, + person=membership.member, + description=_("Manual payment for contribution {membership_type} ({begin_year}/{end_year}) (migrated)").format( + membership_type=membership_type_name(membership.member, membership.type), + begin_year=membership.year, end_year=(membership.year + 1) + ), + payment_datetime=transaction_date, + created_by=None + ) + num_settlements_created += 1 + # If the membership was paid with Annual Authorization, but an unpaid ContributionTransaction exists, + # then EITHER that payment was created manually after the Pre-SEPA date and is wrong. It needs to remain unpaid. + # OR it was created BEFORE the Pre-SEPA date, and it is an old transaction. It needs to be marked paid with the Pre-SEPA method. + elif hasattr(membership, 'payment') and membership.payment.payment_type == authorization_payment_type: + unpaid_transactions = membership.contributiontransaction_set.filter(price__gt=0, settlement=None) + if unpaid_transactions.exists(): + # Check if pre-sepa + date = membership.payment.date or datetime.date(membership.year, 9, 1) + if date < pre_sepa_begin_date: + # Correct, needs to be marked as paid + if membership.payment.date: + transaction_date = datetime.datetime.combine( + membership.payment.date, datetime.time(0, 0) + ).replace(tzinfo=timezone.get_default_timezone()) + else: + # Use the pre-sepa transaction date. + transaction_date = pre_sepa_transaction_date + # Create a ManualPayment settlement with the Pre-SEPA payment method. + create_settlement_for_transactions( + ManualPaymentSettlement, + SettlementManualPaymentTransaction, + transactions=unpaid_transactions, + payment_method=pre_sepa_payment_method, + person=membership.member, + description=_("Pre-SEPA payment for contribution {membership_type} ({begin_year}/{end_year}) (migrated)").format( + membership_type=membership_type_name(membership.member, membership.type), + begin_year=membership.year, end_year=(membership.year + 1) + ), + payment_datetime=transaction_date, + created_by=None + ) + num_settlements_created += 1 + else: + # Manually created after the pre-sepa date. Needs to be left unpaid. + num_left_unpaid += 1 + else: + num_left_unpaid += 1 + logger.info(f'- [DONE] Created {num_cts_created} ContributionTransactions, {num_settlements_created} settlements, settled {created_internal_settlements} reversals and left {num_left_unpaid} memberships unpaid.') + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0023_documenso_id_signed_document'), + ('personal_tab', '0014_personaltabsettlement_and_more'), + ] + + operations = [ + # Create ContributionTransactions for all memberships that do not have one, + # and add a Manual Payments for all memberships that have a Payment that is not direct debit. + migrations.RunPython(forwards), + migrations.DeleteModel( + name='Payment', + ), + migrations.DeleteModel( + name='PaymentType', + ), + ] diff --git a/amelie/members/models.py b/amelie/members/models.py index 4aa52ebf..f874a0e2 100644 --- a/amelie/members/models.py +++ b/amelie/members/models.py @@ -2,14 +2,14 @@ import datetime import logging -import re import uuid import os +from decimal import Decimal from io import BytesIO from typing import Optional - from colorfield.fields import ColorField + from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError @@ -17,11 +17,13 @@ from django.core.validators import RegexValidator, MinLengthValidator, MaxLengthValidator, MaxValueValidator, \ MinValueValidator from django.db import models, transaction -from django.db.models import Q, F +from django.db.models import Q, F, Sum from django.db.models.signals import post_save, m2m_changed from django.template.defaultfilters import slugify from django.urls import reverse +from django.utils import translation, timezone from django.utils.translation import get_language +from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _l from documenso_sdk import EnvelopeDistributeResponse @@ -432,10 +434,11 @@ def is_active_member(self): is_active_member.boolean = True def is_member_strict(self): - member = self.membership_set.filter(Q(type__price=0) | Q(payment__isnull=False), - year=current_association_year()) - member = member.filter(models.Q(ended__isnull=True) | models.Q(ended__gt=datetime.date.today())) - return member.count() > 0 + memberships = self.membership_set.filter( + Q(ended__isnull=True) | Q(ended__gt=datetime.date.today()), + year=current_association_year() + ) + return any(m.type.price == 0 or m.is_paid() for m in memberships) is_member_strict.boolean = True @@ -604,6 +607,16 @@ def has_mandate_other_payments(self): """ return self.has_mandate('other_payments') + def unpaid_transactions(self, transaction_type: Optional[str] = None): + if not transaction_type: + return self.transaction_set.filter(settlement=None) + if transaction_type not in ['activity', 'alexia', 'contribution', 'cookiecorner', 'custom', 'reversal']: + raise ValueError(_('Invalid transaction type')) + return self.transaction_set.filter(**{f'{transaction_type}transaction__isnull': False, f'{transaction_type}transaction__settlement': None}) + + def unpaid_transactions_total_cost(self, transaction_type: Optional[str] = None) -> Decimal: + return self.unpaid_transactions(transaction_type=transaction_type).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") + def oauth_consumer_set(self): consumers = {} for auth in self.oauth_authorization_set.order_by('-last_use').all(): @@ -841,22 +854,6 @@ class Meta(object): def __str__(self): return '{} (€{})'.format(self.name, self.price) -class PaymentType(models.Model): - """ - e.g. Cash or Debit transaction - """ - name = models.CharField(max_length=20, unique=True, verbose_name=_l('Name')) - description = models.TextField(verbose_name=_l('Description')) - visible = models.BooleanField(default=True, verbose_name=_l('Visible')) - - class Meta(object): - ordering = ['description'] - verbose_name = _l('way of payment') - verbose_name_plural = _l('ways of payment') - - def __str__(self): - return '%s' % self.description - class Student(models.Model): """ @@ -1005,11 +1002,95 @@ def get_as_pdf(self) -> bytes: pdf = buffer.getvalue() return pdf - def is_paid(self): - try: - return self.payment - except Payment.DoesNotExist: - return self.type.price == 0 + def is_free(self) -> bool: + """ + Returns if this membership is free and thus requires no payment. + """ + return self.type.price == 0 + + def is_paid(self) -> bool: + """ + A membership is paid if it costs nothing, or if all ContributionTransactions that exist for it are paid. + """ + if self.is_free(): + return True + else: + # ContributionTransactions are created when the membership is created but also when the + # direct debit collection is reversed. So you can have multiple, that all need to be paid. + return self.contributiontransaction_set.exists() and all(t.is_paid() for t in self.contributiontransaction_set.all()) + + def payment_transaction(self): + """ + Returns the most recently created ContributionTransaction for this membership. + """ + return self.contributiontransaction_set.order_by('-pk').first() + + def payment_details(self): + """ + Returns information on how this membership was paid or how it might probably be paid in the future. + Used mainly on the membership details tab to quickly show the payment status and method of a Membership. + + If the membership is free, it returns ('free', None). + Else, see the docstring of `personal_tab.transactions.get_transaction_payment_status`. + """ + from amelie.personal_tab.transactions import get_transaction_payment_status # Avoid circular import + # If the participation is free, it returns None. + if self.is_free(): + return 'free', None + + current_transaction = self.payment_transaction() + return get_transaction_payment_status(transaction=current_transaction, mandate_type='contribution') + + @transaction.atomic + def create_contribution_transaction(self, date: Optional[datetime.datetime] = None): + """ + Create a ContributionTransaction if none exist and if the member needs to pay for this membership. + """ + from amelie.personal_tab.models import ContributionTransaction # Avoid circular import + if date is None: + date = timezone.now() + if not self.contributiontransaction_set.exists() and self.type.price != 0: + with translation.override(self.member.preferred_language): + ct = ContributionTransaction( + date=date, price=self.type.price, person=self.member, membership=self, settlement=None, + description=_('Contribution {membership_type} ({begin_year}/{end_year})').format( + membership_type=self.type.name, + begin_year=self.year, + end_year=(self.year + 1) + ) + ) + ct.save() + return ct + return None + + @transaction.atomic + def mark_as_paid(self, payment_method, actor: Optional[Person] = None): + """ + Mark any unpaid ContributionTransaction(s) for this membership as paid, with the given payment method. + """ + # Avoid circular import + from amelie.personal_tab.models import ManualPaymentSettlement + + # Get the ContributionTransaction(s) for this Membership that still need to be paid + cts = [t for t in self.contributiontransaction_set.all() if not t.is_paid()] + if not cts: + return None + + # Override language to get the description strings in the person's preferred language + with translation.override(self.member.preferred_language): + # Create a ManualPayment settlement with the given payment method to mark the contribution transaction as paid. + description = _("Payment for contribution {membership_type} ({begin_year}/{end_year})").format( + membership_type=self.type.name, begin_year=self.year, end_year=(self.year + 1) + ) + settlement = ManualPaymentSettlement.create_for_transactions( + transactions=cts, + payment_method=payment_method, + person=self.member, + settlement_description=description, + payment_description=description, + created_by=actor + ) + return settlement def is_verified(self): if self.type.needs_verification and self.member.membership_set.filter(ended__isnull=True, year=current_association_year()+1).count() == 0: @@ -1054,7 +1135,7 @@ def process_signed_document(self): raise ValueError(_l("Documenso ID is not set. No documents are due to be signed.")) # Retrieve the document and save it to the database from amelie.tools.documenso import retrieve_documents - documents, _ = retrieve_documents(self.documenso_id) + documents, unused = retrieve_documents(self.documenso_id) if len(documents) > 1: logging.getLogger(__name__).warning(f"Received multiple documents from Documenso for {self}. Only saving the first one.") self.signed_document = ContentFile(content=documents[0].data, name=documents[0].filename) @@ -1067,27 +1148,6 @@ def documenso_url(self): return "#" -class Payment(models.Model): - """ - Payment of a Membership. - Please note that processing properties of this model may be subject to privacy regulations. Refer to - https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. - """ - date = models.DateField(null=True, verbose_name=_l('Date')) - payment_type = models.ForeignKey(PaymentType, verbose_name=_l('Payment'), on_delete=models.PROTECT) - amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name=_l('Price')) - - membership = models.OneToOneField(Membership, verbose_name=_l('Membership'), on_delete=models.PROTECT) - - class Meta(object): - ordering = ['date'] - verbose_name = _l('payment') - verbose_name_plural = _l('payments') - - def __str__(self): - return '%s (%.2f)' % (self.membership, self.amount) - - class CommitteeCategory(models.Model): """ Category in which a committee can be placed (e.g. Activity committees). @@ -1410,10 +1470,11 @@ def activate_registration(self) -> Person: person.preferences.set(self.preferences.all()) person.save() - # Add membership details + # Add membership details and create a transaction for it if necessary membership = Membership(member=person, type=self.membership_type, year=self.membership_year) membership.save() + membership.create_contribution_transaction() # Add student details student = Student(person=person, number=self.student_number) diff --git a/amelie/members/query_forms.py b/amelie/members/query_forms.py index 300c1fe8..5ba38541 100644 --- a/amelie/members/query_forms.py +++ b/amelie/members/query_forms.py @@ -41,7 +41,7 @@ class QueryForm(forms.Form): member = forms.BooleanField(required=False, label=_l('Return former members')) old_member = forms.BooleanField(required=False, label=_l('Only return former members')) - paid = forms.BooleanField(required=False, label=_l('Has not paid yet')) + unpaid = forms.BooleanField(required=False, label=_l('Has not paid yet')) member_in_year = forms.MultipleChoiceField(required=False, label=_l('Was a member in one of these years')) study = forms.ModelChoiceField(Study.objects.all(), required=False) @@ -191,8 +191,10 @@ def filter(self, persons): persons = persons.exclude(membership__in=Membership.objects.filter( Q(ended__gt=date.today()) | Q(ended__isnull=True), year=current_association_year())) - if cleaned_data['paid']: - memberships = memberships.filter(type__price__gt=0, payment__isnull=True) + if cleaned_data['unpaid']: + # Usually payment of a transaction cannot be determined with a filter, but only with the `.is_paid()` method. + # This shortcut works here because a new ContributionTransaction (without a settlement) is created each time a payment is reversed. + memberships = memberships.filter(type__price__gt=0, contributiontransaction__settlement__isnull=True) if cleaned_data['membership']: memberships = memberships.filter(type__in=cleaned_data['membership']) diff --git a/amelie/members/templates/includes/query/query_membership.html b/amelie/members/templates/includes/query/query_membership.html index 236c887c..70939f7c 100644 --- a/amelie/members/templates/includes/query/query_membership.html +++ b/amelie/members/templates/includes/query/query_membership.html @@ -4,7 +4,7 @@

{% trans 'Membership' %}

- {% fieldset membership,member,not_verified_membership,verified_membership,old_member,paid,member_in_year as form_membership from form %} + {% fieldset membership,member,not_verified_membership,verified_membership,old_member,unpaid,member_in_year as form_membership from form %} {{ form_membership }}
diff --git a/amelie/members/templates/members/payment_confirm_delete.html b/amelie/members/templates/members/payment_confirm_delete.html deleted file mode 100644 index ad33bb0b..00000000 --- a/amelie/members/templates/members/payment_confirm_delete.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "basis.html" %} -{% load i18n %} - -{% block titel %} - {% trans 'Remove payment' %} -{% endblock titel %} - -{% block content %} -
-

{% blocktrans %}Remove payment{% endblocktrans %}

-
-
{% csrf_token %} -

- {% blocktrans with activity=object.activiteit %} - Are you sure you want to remove the payment {{ object }}? - {% endblocktrans %} -

-

- - - {% trans 'Cancel' %} - -

-
-
-
-{% endblock %} diff --git a/amelie/members/templates/person_membership.html b/amelie/members/templates/person_membership.html index 4b98764c..e9069a60 100644 --- a/amelie/members/templates/person_membership.html +++ b/amelie/members/templates/person_membership.html @@ -44,7 +44,7 @@ {% trans 'Course year' %} {% trans 'Sort' %} - {% trans 'Payment' %} + {% trans 'Paid?' %} {% if request.is_board %}{% endif %} @@ -68,21 +68,74 @@ {{ membership.type.name }} - {% if membership.payment %} - {{ membership.payment.payment_type.name }} - {% if membership.payment.amount != 0 %} - {% if membership.contributiontransaction_set.count == 0 %} - {% trans 'Delete payment' %} - {% endif %} + {% if membership.is_free %} + {% trans 'n.a.' %} +   + {% if membership.contributiontransaction_set.exists %} + + + {% endif %} -
- {{ membership.payment.date|date:"j b Y"|default:"?" }} - {% endif %} - - {% if not membership.is_paid %} - - {% trans 'Process payment' %} - + {% else %} + {% with membership.payment_details as details %} + {% if membership.is_paid %} + {% trans 'Paid' %} + {% else %} + {# Cannot use brackets in jinja templates, so we need to list the same block twice. #} + {% if details.0 == 'unpaid' and details.1 != None %} + {% trans 'To be debited' %} + {% elif details.0 == 'reversed' %} + {% trans 'To be debited' %} + {% else %} + {% trans 'Not paid yet' %} + {% endif %} + {% endif %} +   + + + {% if details.0 == 'no_transaction' %} +
{% trans 'No transaction! Contact the WWW.' %} + {% elif details.0 == 'paid' %} + {% if details.1.settlement_type == "INSTR" %} +
+ {% blocktrans with date=details.1.batch.execution_date|date:"Y-m-d" %} + Debited on {{ date }} + {% endblocktrans %} + + {% elif details.1.settlement_type == "MANUAL" %} +
+ {% blocktrans with method=details.1.payment_method.name date=details.1.payment_date|date:"Y-m-d" %} + {{ method }} on {{ date }} + {% endblocktrans %} + + {% else %} +
+ {% trans 'Unknown payment! Contact the WWW.' %} + + {% endif %} + {% elif details.0 == 'reversed' %} +
+ {% trans 'Previous attempt was reversed' %} + + {% elif details.0 == 'unpaid' %} + {% if details.1 == None %} +
+ {% trans 'Needs manual payment' %} + + {% endif %} + {% endif %} + {% if details.0 == 'unpaid' and details.1 == None %} +
+ {% trans 'Enter manual payment' %} + + {% endif %} + {% endwith %} {% endif %} {% if request.is_board %} diff --git a/amelie/members/templates/person_membership_create_payment.html b/amelie/members/templates/person_membership_create_payment.html new file mode 100644 index 00000000..1998f918 --- /dev/null +++ b/amelie/members/templates/person_membership_create_payment.html @@ -0,0 +1,73 @@ +{% load humanize %} +{% load i18n %} +
+

+ {% blocktrans with membership=membership person=person %} + Create manual payment for {{ membership }} of {{ person }} + {% endblocktrans %} +

+ +

{% trans 'You are creating a manual payment that will cover the following transactions:' %}

+ +
+ + + + + + + + + + {% for transaction in transactions %} + + + + + + {% empty %} + + + + {% endfor %} + + + + + + +
{% trans 'Timestamp' %}{% trans 'Description' %}{% trans 'Amount' %}
+ {{ transaction.date|date:"d-m-Y H:i" }} + + + {{ transaction.description }} + + + € {{ transaction.price|intcomma }} +
+ {% trans 'No transactions found. This is probably wrong! Please contact the WWW.' %} +
{% trans 'Total' %}€ {{ transactions_total|intcomma }}
+
+
+

{% trans 'Please select the payment method that was used for the payment.' %}

+ +
+ {% csrf_token %} + {{ form }} + +
+ + {% trans 'Cancel' %} + + + + +
+
+
+
diff --git a/amelie/members/templates/person_membership_payments.html b/amelie/members/templates/person_membership_payments.html new file mode 100644 index 00000000..713e1035 --- /dev/null +++ b/amelie/members/templates/person_membership_payments.html @@ -0,0 +1,72 @@ +{% load humanize %} +{% load i18n %} +
+ {# Messages to show if we redirected here from the create manual payment view #} + {% if status_msg == "created" %} +
    +
  • + {% trans 'Payment created. Please check the transactions below for correctness.' %} +
  • +
+ {% elif status_msg == "no_transactions" %} +
    +
  • + {% trans 'No unpaid transactions found. Please re-check and try again.' %} +
  • +
+ {% endif %} + +

+ {% blocktrans with membership=membership.type.name year_1=membership.year year_2=membership.year|add:1 %} + Payments for {{ membership }} {{ year_1 }}/{{ year_2 }} + {% endblocktrans %} +   + {% if membership.is_paid %} + ({% trans 'Paid' %}) + {% else %} + ({% trans 'Not paid yet' %}) + {% endif %} +

+ +
+ + + + + + + + + + + {% for transaction in transactions %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans 'Timestamp' %}{% trans 'Description' %}{% trans 'Amount' %}{% trans 'Settled in' %}
+ {{ transaction.date|date:"d-m-Y H:i" }} + + + {{ transaction.description }} + + + € {{ transaction.price|intcomma }} + + {% include 'info/transaction_settlement_info.html' with settlement=transaction.settlement highlight_unpaid=True %} +
+ {% trans 'No transactions found. If this is a paid membership, this is probably wrong! Please contact the WWW.' %} +
+
+ + {% trans 'Return' %} + +
diff --git a/amelie/members/templates/person_payment.html b/amelie/members/templates/person_payment.html deleted file mode 100644 index f408eca1..00000000 --- a/amelie/members/templates/person_payment.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load i18n %} -
-

{% trans 'Execute payment' %}

- -
- {% csrf_token %} -

- {{ membership.year }} ({{ membership.type }}, EUR {{ membership.type.price }}) -

- - {{ form }} - - -
-
diff --git a/amelie/members/templates/statistics/payments.html b/amelie/members/templates/statistics/payments.html deleted file mode 100644 index 238d5bf1..00000000 --- a/amelie/members/templates/statistics/payments.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "basis.html" %} -{% load i18n %} - -{% block titel %} - {% trans 'Payment statistics' %} -{% endblock titel %} - -{% block content %} -
-
-

{% trans "Payments" %}

-
- - - - {% for ptype in payment_types %} - - {% endfor %} - - - {% for row in rows %} - - {% for cell in row %} - - {% endfor %} - - {% endfor %} - - - {% for totalcell in totalcells %} - - {% endfor %} - -
{{ ptype.name }}{% trans 'Total' %}
{{ cell }}
{% trans 'Totals' %}{{ totalcell }}
-
-
-
-{% endblock content %} diff --git a/amelie/members/views.py b/amelie/members/views.py index 7854b204..29ca2a07 100644 --- a/amelie/members/views.py +++ b/amelie/members/views.py @@ -6,7 +6,6 @@ import uuid from datetime import date -from datetime import timezone as tz from decimal import Decimal from functools import lru_cache from io import BytesIO @@ -32,7 +31,7 @@ from django.shortcuts import get_object_or_404, render, redirect from django.utils import timezone, translation from django.utils.translation import gettext as _ -from django.views.generic.edit import DeleteView, FormView +from django.views.generic.edit import FormView from amelie.claudia.models import Mapping, ExtraPerson from amelie.iamailer import MailTask @@ -44,7 +43,7 @@ RegistrationFormStepPersonalPreferences, RegistrationFormStepFinalCheck, RegistrationFormStepGeneralStudyDetails, \ RegistrationFormStepFreshmenMembershipDetails, RegistrationFormStepEmployeeDetails, \ RegistrationFormPersonalDetailsEmployee, RegistrationFormStepEmployeeMembershipDetails, PreRegistrationPrintAllForm -from amelie.members.models import Payment, PaymentType, Committee, Function, Membership, MembershipType, Employee, \ +from amelie.members.models import Committee, Function, Membership, MembershipType, Employee, \ Person, Student, Study, StudyPeriod, Preference, PreferenceCategory, UnverifiedEnrollment, Dogroup, \ DogroupGeneration from amelie.personal_tab.forms import RFIDCardForm @@ -54,7 +53,7 @@ from amelie.tools.encodings import normalize_to_ascii from amelie.tools.http import HttpResponseSendfile, HttpJSONResponse, is_allowed_ip from amelie.tools.logic import current_academic_year_with_holidays, current_association_year, association_year -from amelie.tools.mixins import DeleteMessageMixin, RequireBoardMixin, RequireCommitteeMixin, RequireAllowlistedIPMixin +from amelie.tools.mixins import RequireCommitteeMixin, RequireAllowlistedIPMixin from amelie.tools.mail import PersonRecipient from amelie.tools.pdf import pdf_separator_page, pdf_membership_page, pdf_authorization_page @@ -288,26 +287,6 @@ def statistics(request): return render(request, 'statistics/overview.html', locals()) -@require_board -def payment_statistics(request, start_year=2012): - rows = [] - payment_types = PaymentType.objects.all() - membership_types = MembershipType.objects.all() - for mtype in membership_types: - cells = [mtype, ] - count = 0 - for ptype in payment_types: - memberships = Membership.objects.filter(year=start_year, payment__payment_type=ptype, type=mtype) - mcount = memberships.count() - count += mcount - cells.append(mcount) - cells.append(count) - rows.append(cells) - ziprows = zip(*rows) - totalcells = map(sum, list(zip(*rows))[1:]) - return render(request, 'statistics/payments.html', locals()) - - @require_committee(settings.ROOM_DUTY_ABBREVIATION) def person_view(request, id, slug): obj = get_object_or_404(Person, id=id, slug=slug) @@ -353,10 +332,7 @@ def _person_can_be_anonymized(person): "({year}/{next_year} {type}).".format( year=membership.year, next_year=membership.year + 1, type=membership.type ))) - # Date where new SEPA authorizations came into effect. - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) - personal_tab_credit = Transaction.objects.filter(person=person, date__gte=begin).aggregate(Sum('price'))[ - 'price__sum'] or Decimal('0.00') + personal_tab_credit = Transaction.objects.filter(person=person).aggregate(Sum('price'))['price__sum'] or Decimal('0.00') if personal_tab_credit != 0: unable_to_anonymize_reasons.append(_("This person still has an outstanding balance on their personal tab.")) @@ -577,10 +553,11 @@ def done(self, form_list, **kwargs): start_date=date.today()) authorization_other.save() - # Add membership details + # Add membership details and create a transaction for it if necessary membership = Membership(member=person, type=cleaned_data['membership'], year=current_association_year()) membership.save() + membership.create_contribution_transaction() # Add student details student = Student(person=person, number=cleaned_data['student_number']) @@ -692,10 +669,11 @@ def done(self, form_list, **kwargs): start_date=date.today()) authorization_other.save() - # Add membership details + # Add membership details and create a transaction for it if necessary membership = Membership(member=person, type=MembershipType.objects.get(name_en='Primary yearlong'), year=current_association_year()) membership.save() + membership.create_contribution_transaction() # Create user for logging in person.get_or_create_user(f"e{person.pk}") @@ -805,10 +783,11 @@ def done(self, form_list, **kwargs): start_date=date.today()) authorization_other.save() - # Add membership details + # Add membership details and create a transaction for it if necessary membership = Membership(member=person, type=cleaned_data['membership'], year=current_association_year()) membership.save() + membership.create_contribution_transaction() # Add employee details employee = Employee(person=person, number=cleaned_data['employee_number']) @@ -923,10 +902,11 @@ def done(self, form_list, **kwargs): start_date=date.today()) authorization_other.save() - # Add membership details + # Add membership details and create a transaction for it if necessary membership = Membership(member=person, type=cleaned_data['membership'], year=current_association_year()) membership.save() + membership.create_contribution_transaction() # Add student details student = Student(person=person, number=cleaned_data['student_number']) @@ -1664,27 +1644,6 @@ def books_list(request): 'student_numbers_other': student_numbers_other, }) -class PaymentDeleteView(RequireBoardMixin, DeleteMessageMixin, DeleteView): - model = Payment - template_name = "members/payment_confirm_delete.html" - - def get_object(self, queryset=None): - payment = super(PaymentDeleteView, self).get_object() - # You are not allowed to delete 'zero'-memberships manually - if payment.amount == 0.0: - raise PermissionDenied(_("You are not allowed to manually delete the payments of free memberships.")) - # You are not allowed to delete payments that are (currently) debited manually - if payment.membership.contributiontransaction_set.count() > 0: - raise PermissionDenied(_("You are not allowed to delete payments that are already (being) debited.")) - return payment - - def get_delete_message(self): - return _('Payment %(object)s has been removed' % {'object': self.get_object()}) - - def get_success_url(self): - return self.object.membership.member.get_absolute_url() - - class DoGroupTreeView(TemplateView): template_name = "dogroups.html" diff --git a/amelie/personal_tab/admin.py b/amelie/personal_tab/admin.py index d140de62..2d7f0cb1 100644 --- a/amelie/personal_tab/admin.py +++ b/amelie/personal_tab/admin.py @@ -4,7 +4,8 @@ CustomTransaction, ActivityTransaction, CookieCornerTransaction, AlexiaTransaction, ContributionTransaction, \ Article, Category, RFIDCard, AuthorizationType, Authorization, Amendment, DebtCollectionAssignment, \ DebtCollectionBatch, \ - DebtCollectionInstruction, Reversal, DebtCollectionTransaction, ReversalTransaction, LedgerAccount, PrintLogEntry + DebtCollectionInstruction, Reversal, DebtCollectionTransaction, ReversalTransaction, LedgerAccount, PrintLogEntry, \ + PaymentMethod class RFIDAdmin(admin.ModelAdmin): @@ -19,7 +20,7 @@ class CookieCornerTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['article__name_nl', 'article__name_en', 'person__slug'] - raw_id_fields = ('person', 'discount', 'debt_collection',) + raw_id_fields = ('person', 'discount', 'settlement',) readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -27,13 +28,13 @@ class CookieCornerTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(CookieCornerTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(CookieCornerTransactionAdmin, self).has_delete_permission(request, obj) @@ -44,7 +45,7 @@ class TransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['description', 'person__slug'] - raw_id_fields = ('person', 'discount', 'debt_collection',) + raw_id_fields = ('person', 'discount', 'settlement',) readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -55,13 +56,13 @@ def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(TransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(TransactionAdmin, self).has_delete_permission(request, obj) @@ -72,7 +73,7 @@ class CustomTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['description', 'person__slug'] - raw_id_fields = ('person', 'discount', 'debt_collection',) + raw_id_fields = ('person', 'discount', 'settlement',) readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -80,13 +81,13 @@ class CustomTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(CustomTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(CustomTransactionAdmin, self).has_delete_permission(request, obj) @@ -97,7 +98,7 @@ class ActivityTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on', 'with_enrollment_options') date_hierarchy = 'date' search_fields = ['description', 'person__slug', 'event__summary_nl', 'event__summary_en'] - raw_id_fields = ('person', 'discount', 'debt_collection', 'event', 'participation') + raw_id_fields = ('person', 'discount', 'settlement', 'event', 'participation') readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -105,13 +106,13 @@ class ActivityTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(ActivityTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(ActivityTransactionAdmin, self).has_delete_permission(request, obj) @@ -122,7 +123,7 @@ class AlexiaTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['description', 'person__slug', 'transaction_id'] - raw_id_fields = ('person', 'discount', 'debt_collection') + raw_id_fields = ('person', 'discount', 'settlement') readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -130,13 +131,13 @@ class AlexiaTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(AlexiaTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(AlexiaTransactionAdmin, self).has_delete_permission(request, obj) @@ -147,7 +148,7 @@ class ContributionTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['description', 'person__slug'] - raw_id_fields = ('person', 'membership', 'discount', 'debt_collection') + raw_id_fields = ('person', 'membership', 'discount', 'settlement') readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -155,13 +156,13 @@ class ContributionTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(ContributionTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(ContributionTransactionAdmin, self).has_delete_permission(request, obj) @@ -172,7 +173,7 @@ class DebtCollectionTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['description', 'person__slug'] - raw_id_fields = ('person', 'discount', 'debt_collection') + raw_id_fields = ('person', 'discount', 'settlement') readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -180,13 +181,13 @@ class DebtCollectionTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(DebtCollectionTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(DebtCollectionTransactionAdmin, self).has_delete_permission(request, obj) @@ -197,7 +198,7 @@ class ReversalTransactionAdmin(admin.ModelAdmin): list_filter = ('date', 'added_on') date_hierarchy = 'date' search_fields = ['description', 'person__slug'] - raw_id_fields = ('person', 'reversal', 'discount', 'debt_collection') + raw_id_fields = ('person', 'reversal', 'discount', 'settlement') readonly_fields = ('added_on', 'added_by') ordering = ['-added_on'] @@ -205,13 +206,13 @@ class ReversalTransactionAdmin(admin.ModelAdmin): actions = None def has_change_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(ReversalTransactionAdmin, self).has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - if obj and obj.debt_collection: + if obj and obj.settlement: return False else: return super(ReversalTransactionAdmin, self).has_delete_permission(request, obj) @@ -304,6 +305,12 @@ def has_add_permission(self, request): return False +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ('id', 'name_nl', 'name_en', 'description_nl', 'description_en', 'visible', 'visible_memberships', 'visible_activities', 'frontend_icon_display') + list_filter = ('visible', 'visible_memberships', 'visible_activities') + search_fields = ('name_nl', 'name_en', 'description_nl', 'description_en') + + class DiscountPeriodAdmin(admin.ModelAdmin): list_display = ('id', 'description_nl', 'description_en', 'begin', 'end', 'ledger_account_number', 'balance_account_number') @@ -380,6 +387,7 @@ class DeclarationAdmin(admin.ModelAdmin): admin.site.register(DebtCollectionBatch, DebtCollectionBatchAdmin) admin.site.register(DebtCollectionInstruction, DebtCollectionInstructionAdmin) admin.site.register(Reversal, ReversalAdmin) +admin.site.register(PaymentMethod, PaymentMethodAdmin) admin.site.register(DiscountPeriod, DiscountPeriodAdmin) admin.site.register(Discount, DiscountAdmin) admin.site.register(DiscountCredit, DiscountCreditAdmin) diff --git a/amelie/personal_tab/debt_collection.py b/amelie/personal_tab/debt_collection.py index 53011ac9..d43f80cd 100644 --- a/amelie/personal_tab/debt_collection.py +++ b/amelie/personal_tab/debt_collection.py @@ -1,12 +1,13 @@ import datetime from datetime import timezone as tz +from django.db import transaction from django.db.models import Sum, Q from django.template.defaultfilters import date as _date from django.utils import timezone, translation from django.utils.translation import gettext as _ -from amelie.members.models import Person, Membership, PaymentType, Payment +from amelie.members.models import Person, Membership from amelie.personal_tab.models import Transaction, DebtCollectionInstruction, DebtCollectionBatch, \ DebtCollectionTransaction, ContributionTransaction, ReversalTransaction, Reversal, Amendment from amelie.tools.encodings import normalize_to_ascii @@ -25,8 +26,6 @@ def authorization_contribution(person): :type person: Person """ - # 2 Contribution - # 4 Contribution (old version) authorizations = person.authorization_set.filter(is_signed=True, end_date__isnull=True, authorization_type__contribution=True).order_by('authorization_type__id') return authorizations[0] if authorizations else None @@ -48,10 +47,6 @@ def authorization_cookie_corner(person): :type person: Person """ - # 1 Consumptions, activities and other - # 4 Consumptions and activities - # 5 Consumptions - authorizations = person.authorization_set.filter( is_signed=True, end_date__isnull=True, authorization_type__consumptions=True ).order_by('authorization_type__id') @@ -74,19 +69,17 @@ def generate_contribution_instructions(years): The DebtCollectionInstruction objects that are returned are not saved yet. """ - memberships = Membership.objects.filter(Q(ended__gt=datetime.date.today()) | Q(ended__isnull=True), - payment__isnull=True, type__price__gt=0, year__in=years) - result = { 'ongoing_frst': [], 'frst': [], 'rcur': [] } - for m in memberships: - person = m.member - price = m.type.price - sumf = ("%.2f" % price).replace('.', ',') + unpaid_membership_transactions = ContributionTransaction.objects.filter( + settlement=None, membership__type__price__gt=0, membership__year__in=years + ) + for umt in unpaid_membership_transactions: + person = umt.membership.member authorization = authorization_contribution(person) @@ -98,22 +91,23 @@ def generate_contribution_instructions(years): with translation.override(person.preferred_language): description = _('Contribution Inter-Actief {membership_year} {membership_type} {name} Questions? Email treasurer@inter-actief.net')\ - .format(membership_year="{}-{}".format(m.year, m.year + 1), membership_type=m.type.name, name=name) + .format(membership_year="{}-{}".format(umt.membership.year, umt.membership.year + 1), membership_type=umt.membership.type.name, name=name) description = normalize_to_ascii(description) instruction = None sequence_type = authorization.next_sequence_type() if sequence_type: - instruction = DebtCollectionInstruction(amount=price, authorization=authorization, description=description, - amendment=authorization.next_amendment()) + instruction = DebtCollectionInstruction( + amount=umt.price, authorization=authorization, description=description, + amendment=authorization.next_amendment() + ) row = { 'person': person, 'authorization': authorization, - 'membership': m, - 'sum': price, - 'sumf': sumf, + 'transaction': umt, + 'sum': umt.price, 'instruction': instruction } @@ -131,7 +125,7 @@ def filter_contribution_instructions(rows, post): res = [] for row in rows: - if 'contribution_%i' % row['membership'].id in post: + if 'contribution_%i' % row['transaction'].membership.id in post: res.append(row) return res @@ -147,18 +141,14 @@ def save_contribution_instructions(rows, batch): debt_collection_datetime = timezone.make_aware(datetime.datetime.combine(execution_date, datetime.time(0, 0)), timezone.get_default_timezone()) # Annual authorization - payment_method = PaymentType.objects.get(id=4) - for row in rows: - person = row['person'] - price = row['sum'] - membership = row['membership'] - instruction = row['instruction'] + person: Person = row['person'] + contribution_transaction: ContributionTransaction = row['transaction'] + instruction: DebtCollectionInstruction = row['instruction'] with translation.override(person.preferred_language): - dct_description = _('Direct withdrawal of contribution {date}').format(date=_date(execution_date, "j F Y")) - ct_description = _('Contribution {membership_type} ({begin_year}/{end_year})').format( - membership_type=membership.type.name, begin_year=membership.year, end_year=(membership.year + 1) + dct_description = _('Direct withdrawal of contribution {date}').format( + date=_date(execution_date, "j F Y") ) instruction.batch = batch @@ -167,25 +157,19 @@ def save_contribution_instructions(rows, batch): instruction.end_to_end_id = instruction.debt_collection_reference() instruction.save() - ct = ContributionTransaction(date=debt_collection_datetime, price=price, person=person, - description=ct_description, membership=membership, debt_collection=instruction) - ct.save() + # Set the contribution transaction settlement, so it is considered paid. + contribution_transaction.settlement = instruction + contribution_transaction.save() - dct = DebtCollectionTransaction(date=debt_collection_datetime, price=-price, person=person, - description=dct_description, debt_collection=instruction) + dct = DebtCollectionTransaction( + date=debt_collection_datetime, price=-instruction.amount, person=person, + description=dct_description, settlement=instruction + ) dct.save() - payment = Payment(date=execution_date, payment_type=payment_method, amount=price, membership=membership) - payment.save() - def generate_cookie_corner_instructions(end_date): - all_transactions = Transaction.objects.filter(debt_collection=None) - - # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin_date = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) - - all_transactions = all_transactions.filter(date__gte=begin_date, date__lt=end_date) + all_transactions = Transaction.objects.filter(settlement=None, date__lt=end_date) # Order only by person, so distinct works as intended. See Django manual. people = all_transactions.filter(person__isnull=False).order_by('person').distinct().values('person') @@ -205,7 +189,6 @@ def generate_cookie_corner_instructions(end_date): person = Person.objects.get(id=p['person']) transactions = all_transactions.filter(person=person) price = transactions.aggregate(Sum('price'))['price__sum'] - sumf = ("%.2f" % price).replace('.', ',') if price == 0: continue @@ -234,7 +217,6 @@ def generate_cookie_corner_instructions(end_date): 'person': person, 'authorization': authorization, 'sum': price, - 'sumf': sumf, 'instruction': instruction, 'transactions': transactions } @@ -294,11 +276,11 @@ def save_cookie_corner_instructions(rows, batch): instruction.save() for transaction in transactions: - transaction.debt_collection = instruction + transaction.settlement = instruction transaction.save() dct = DebtCollectionTransaction(date=debt_collection_datetime, price=-price, person=person, - description=description, debt_collection=instruction) + description=description, settlement=instruction) dct.save() @@ -319,19 +301,26 @@ def process_reversal(reversal, actor): person=person, description=description, added_by=actor) rt.save() - for ct in ContributionTransaction.objects.filter(debt_collection=instruction): - # Only reverse real transactions, do not reverse other reversals. - if ct.price > 0: - Payment.objects.filter(membership__contributiontransaction=ct).delete() + # Filter for real (positive) ContributionTransactions to avoid reversing other reversals. + for ct in ContributionTransaction.objects.filter(settlement=instruction, price__gt=0): + with translation.override(ct.person.preferred_language): + description = _('Reversal contribution {membership_type} ({start_year}/{end_year})').format( + membership_type=ct.membership.type.name, start_year=ct.membership.year, + end_year=ct.membership.year + 1 + ) - with translation.override(ct.person.preferred_language): - description = _('Reversal contribution {membership_type} ({start_year}/{end_year})').format( - membership_type=ct.membership.type.name, start_year=ct.membership.year, - end_year=ct.membership.year + 1 - ) - cct = ContributionTransaction(date=reversal_datetime, price=-ct.price, person=ct.person, - description=description, membership=ct.membership) - cct.save() + # TODO: This next bit is a bit convoluted, but it is the quickest way to get it working nicely. + # Maybe another refactor step is necessary to get rid of all of these extra ContributionTransactions. + # albertskja - 2026-07-22 + + # Create a ContributionTransaction to reverse the original transaction which was marked paid in the debt collection instruction + cct = ContributionTransaction(date=reversal_datetime, price=-ct.price, person=ct.person, + description=description, membership=ct.membership) + cct.save() + # And create another ContributionTransaction to mark that the contribution still needs to be paid. + cct = ContributionTransaction(date=reversal_datetime, price=ct.price, person=ct.person, + description=ct.description, membership=ct.membership) + cct.save() def edit_reversal(reversal, actor): @@ -348,28 +337,33 @@ def edit_reversal(reversal, actor): rt.save() +@transaction.atomic def delete_reversal(reversal): """ :type reversal: Reversal """ - # First we delete the ReversalTransaction and the Reversal object itself. - instruction = reversal.instruction rt = ReversalTransaction.objects.get(reversal=reversal) - rt.delete() - reversal.delete() - # If there were contribution transactions, we need to reinstate payments and delete reversal contribution transactions. - for ct in ContributionTransaction.objects.filter(debt_collection=instruction): - if ct.price > 0: + # TODO: This next bit is a bit convoluted, but we need to clean up what we did in `process_reversal`. + # Maybe another refactor step is necessary to get rid of all of this hassle with ContributionTransactions. + # albertskja - 2026-07-22 + + # If there were any (positive) contribution transactions in the original instruction, we need to delete the + # contribution transactions that were created to reverse that payment and re-incur it. We assume that + # multiple reversals have not been made for the same membership on the same date. + for ct in ContributionTransaction.objects.filter(settlement=reversal.instruction, price__gt=0): + # Delete the reversal ContributionTransactions that were created when the reversal was processed. + ContributionTransaction.objects.filter( + Q(price=-ct.price) | Q(price=ct.price), + date=rt.date, + person=ct.person, membership=ct.membership, + ).delete() + + # Then, delete the ReversalTransaction that re-incurred the costs of the original direct debit to the personal tab. + rt.delete() - # Then we need to reinstate the payment that was deleted when the reversal was processed. - pm = Payment(membership=ct.membership, payment_type=PaymentType.objects.get(id=4), - amount=ct.price, date=ct.date) - pm.save() - - # Now we filter for the reversal ContributionTransaction that was created when the reversal was processed and delete it. - cct = ContributionTransaction.objects.filter(membership=ct.membership, price=-ct.price) - cct.delete() + # Finally, delete the entire Reversal. + reversal.delete() def process_amendment(authorization, date, iban, bic, reason): @@ -414,7 +408,7 @@ def delete_amendment(amendment): Delete an unsent amendment to an authorization. Make sure you call this method only from within a database transaction! - + Make sure you call this method only for amendments that have not yet been sent to the bank! """ diff --git a/amelie/personal_tab/forms.py b/amelie/personal_tab/forms.py index 710f448c..e426e161 100644 --- a/amelie/personal_tab/forms.py +++ b/amelie/personal_tab/forms.py @@ -10,17 +10,17 @@ from django import forms from django.conf import settings from django.db import transaction -from django.db.models import Exists, OuterRef, TextChoices +from django.db.models import TextChoices from django.utils import timezone -from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _l from amelie.members.models import Membership, Person, Committee from amelie.members.forms import clean_iban_and_bic from amelie.personal_tab.transactions import cookie_corner_sale from amelie.personal_tab import statistics -from amelie.personal_tab.models import CustomTransaction, CookieCornerTransaction, Declaration, RFIDCard, Reversal, AuthorizationType, \ - DebtCollectionBatch, Authorization +from amelie.personal_tab.models import CustomTransaction, CookieCornerTransaction, Declaration, RFIDCard, Reversal, \ + AuthorizationType, \ + DebtCollectionBatch, Authorization, ManualPaymentSettlement, PaymentMethod from amelie.tools.http import get_client_ips from amelie.tools.ipp_printer import IPPPrinter from amelie.tools.widgets import DateSelector, DateTimeSelector, MemberSelect @@ -65,8 +65,7 @@ def __init__(self, minimal_execution_date, *args, **kwargs): self.fields['execution_date'].initial = minimal_execution_date # Select the years with unpaid memberships to show contribution_years = Membership.objects.filter( - payment__isnull=True, - type__price__gt=0 + contributiontransaction__settlement=None, type__price__gt=0 ).distinct().order_by('-year').values_list('year', flat=True) self.fields['contribution_years'].choices = [(year, "{}-{}".format(year, year+1)) for year in contribution_years] @@ -128,6 +127,40 @@ def clean(self): return cleaned_data +class ManualPaymentSettlementForm(forms.ModelForm): + payment_date = forms.DateField(label=_l('Payment date'), widget=DateSelector, initial=timezone.now) + payment_method = forms.ModelChoiceField(PaymentMethod.objects.filter(visible=True), label=_l('Payment method'), required=True) + paid_amount = forms.DecimalField( + max_digits=5, decimal_places=2, label=_l('Paid amount'), initial=0, + help_text=_l("The amount that was paid in this settlement. Can be different from the amount that needs to be paid, " + "can even be negative (if money was paid out by the association). " + "Useful if, say, the transactions add up to 9 euro, you can enter a payment of 10 euro, " + "which leaves 1 euro extra as balance on the person's personal tab.")) + + class Meta: + model = ManualPaymentSettlement + fields = ('payment_date', 'payment_method') + + def __init__(self, person: Person, *args, **kwargs): + super().__init__(*args, **kwargs) + self.person = person + + +class ManualPaymentTransactionForm(forms.Form): + id = forms.IntegerField() + include = forms.BooleanField(initial=False, required=False) + + def __init__(self, *args, initial=None, **kwargs): + # initial = {'transaction': t, 'include': False} + if initial is not None: + self.transaction = initial.pop('transaction') + initial['id'] = self.transaction.id + super().__init__(*args, **kwargs) + + +ManualPaymentTransactionFormSet = forms.formset_factory(ManualPaymentTransactionForm, extra=0) + + class SearchAuthorizationForm(forms.Form): class AuthorizationStatuses(TextChoices): UNSIGNED = 'unsigned', _l("Not signed") diff --git a/amelie/personal_tab/management/commands/unpaid_balance_reminder_mail.py b/amelie/personal_tab/management/commands/unpaid_balance_reminder_mail.py new file mode 100644 index 00000000..d193924e --- /dev/null +++ b/amelie/personal_tab/management/commands/unpaid_balance_reminder_mail.py @@ -0,0 +1,65 @@ +from urllib.parse import urljoin + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.urls import reverse + +from amelie.iamailer import MailTask +from amelie.personal_tab.transactions import get_personal_tab_balances +from amelie.tools.const import TaskPriority +from amelie.tools.mail import PersonRecipient + + +class Command(BaseCommand): + help = "Send open balance reminder to members that have a positive open personal tab balance but no authorization." + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser=parser) + parser.add_argument( + '--send', + action='store_true', + dest='send', + default=False, + help='Actually send the e-mails' + ) + + def handle(self, *args, **options): + send_mails = options['send'] + if not send_mails: + self.stderr.write('** TEST MODE **') + self.stderr.write('Add --send to save changes') + + self.stdout.write(self.style.SUCCESS(f'Calculating personal tab balances...')) + + balances_per_person = get_personal_tab_balances(only_members=True, only_without_mandate=True) + task = MailTask(from_="I.C.T.S.V. Inter-Actief ", + template_name='personal_tab/balance_reminder.mail', + report_to="I.C.T.S.V. Inter-Actief ", + report_always=False, + priority=TaskPriority.MEDIUM) + + # Filter balances list to only positive, above minimum amount. + num_recipients = 0 + for bal in balances_per_person: + if bal['person'].pk != 3289: + continue + if bal['balance'] <= settings.PERSONAL_TAB_MINIMUM_BALANCE_REMINDER: + continue # Balance too low for reminder + task.add_recipient(PersonRecipient(bal['person'], context={ + 'balance': bal['balance'], + 'num_unpaid_transactions': bal['unpaid_transactions'].count(), + 'overview_link': urljoin(settings.ABSOLUTE_PATH_TO_SITE, reverse('personal_tab:person_transactions_unpaid', kwargs={ + 'pk': bal['person'].pk, 'slug': bal['person'].slug + })), + })) + num_recipients += 1 + + if num_recipients > 0: + self.stdout.write(self.style.SUCCESS(f'Sending balance reminder emails to {num_recipients} members...')) + if send_mails: + task.send() + else: + self.stdout.write("...but not really... (option --send not given).") + else: + self.stdout.write(self.style.SUCCESS(f'No recipients, not sending e-mails...')) + self.stdout.write("Done") diff --git a/amelie/personal_tab/managers.py b/amelie/personal_tab/managers.py index e4d99632..339c6e0b 100644 --- a/amelie/personal_tab/managers.py +++ b/amelie/personal_tab/managers.py @@ -21,11 +21,7 @@ def _people_with_outstanding_balance(): """ Returns a QuerySet with all Persons having a non-zero cookie corner balance. """ - # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) - - return Person.objects.filter(transaction__date__gte=begin).annotate(balance=Sum('transaction__price')).filter( - balance__gt=0) + return Person.objects.all().annotate(balance=Sum('transaction__price')).filter(balance__gt=0) def _people_with_recent_or_future_transactions(days): diff --git a/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py b/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py new file mode 100644 index 00000000..2cd7cfce --- /dev/null +++ b/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py @@ -0,0 +1,361 @@ +# Written by Kevin Alberts on 2026-07-20 +import datetime +from datetime import timezone as tz +import logging +from decimal import Decimal + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models +from django.utils import translation +from django.utils.translation import gettext as _ + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +# Based on ManualPaymentSettlement.create_settlement_for_transactions method. +def create_settlement_for_transactions(ManualPaymentSettlement, SettlementManualPaymentTransaction, + transactions, payment_method, person, description, + payment_datetime, created_by=None): + transactions = list(transactions) + if not transactions: + raise ValueError("No transactions given! Cannot create an empty manual payment") + + # Calculate the total settlement amount + total_transactions_price = sum(t.price for t in transactions) or Decimal("0.00") + + # Create a ManualPayment settlement with the given payment method. + settlement = ManualPaymentSettlement.objects.create( + payment_date=payment_datetime.date(), + payment_method=payment_method, + description=description[:140], + person=person, + amount=total_transactions_price, + created_by=created_by + ) + + # Save the settlement reference in all transactions + for transaction in transactions: + transaction.settlement = settlement + transaction.save() + + # If transactions come out to a non-zero amount, add a SettlementManualPaymentTransaction for + # the total transaction amount to cancel out the balance of those transactions + if total_transactions_price != 0: + settlement_transaction = SettlementManualPaymentTransaction( + date=payment_datetime, price=-total_transactions_price, person=person, + description=description[:200], settlement=settlement + ) + settlement_transaction.save() + # Register this manual payment as the payment transaction for the settlement + settlement.manual_payment_transaction = settlement_transaction + settlement.save() + return settlement + + +def import_payment_methods_forwards(apps, schema_editor): + """Copy each old members.PaymentType into personal_tab.PaymentMethod, preserving PKs.""" + PaymentType = apps.get_model('members', 'PaymentType') + PaymentMethod = apps.get_model('personal_tab', 'PaymentMethod') + + logger.info('Migrating old members.PaymentType to new personal_tab.PaymentMethod...') + total_count = PaymentType.objects.count() + for i, old in enumerate(PaymentType.objects.all().order_by('pk')): + logger.info(f'- [{i+1}/{total_count}] Processing {old.pk} - {old.name}...') + # Create the new inherited DebtCollectionInstruction, re-using the same ID. + PaymentMethod.objects.create( + id=old.id, + name_en=old.name, + name_nl=old.name, + description_en=old.description, + description_nl=old.description, + visible=old.visible, + visible_memberships=old.visible, + visible_activities=old.visible + ) + + +def import_payment_methods_backwards(apps, schema_editor): + """Reverse: Delete all PaymentMethods""" + PaymentMethod = apps.get_model('personal_tab', 'PaymentMethod') + PaymentMethod.objects.all().delete() + + +def migrate_debt_collection_instructions_forwards(apps, schema_editor): + """Copy each old DebtCollectionInstruction into PersonalTabSettlement + new DebtCollectionInstruction, preserving PKs.""" + OldDebtCollectionInstruction = apps.get_model('personal_tab', 'OldDebtCollectionInstruction') + DebtCollectionInstruction = apps.get_model('personal_tab', 'DebtCollectionInstruction') + + logger.info('Migrating old debt collections to new model...') + total_count = OldDebtCollectionInstruction.objects.count() + for i, old in enumerate(OldDebtCollectionInstruction.objects.all().order_by('pk')): + if i % 1000 == 0: + logger.info(f'- [{i}/{total_count}] Processing ID {old.id}...') + # Create the new inherited DebtCollectionInstruction, re-using the same ID. + DebtCollectionInstruction.objects.create( + id=old.id, + description=old.description, + amount=old.amount, + end_to_end_id=old.end_to_end_id, + amendment=old.amendment, + authorization=old.authorization, + batch=old.batch, + ) + + +def migrate_debt_collection_instructions_backwards(apps, schema_editor): + """Reverse: flatten PersonalTabSettlement + DebtCollectionInstruction back into a single table.""" + DebtCollectionInstruction = apps.get_model('personal_tab', 'DebtCollectionInstruction') + OldDebtCollectionInstruction = apps.get_model('personal_tab', 'OldDebtCollectionInstruction') + + logger.info('Reverting new debt collections to old model...') + for dci in DebtCollectionInstruction.objects.all(): + logger.info(f'- {dci.id} - {dci.amount} eur') + OldDebtCollectionInstruction.objects.create( + id=dci.id, + description=dci.description, # comes from the parent via multi-table inheritance + amount=dci.amount, # comes from the parent via multi-table inheritance + end_to_end_id=dci.end_to_end_id, + amendment=dci.amendment, + authorization=dci.authorization, + batch=dci.batch, + ) + + +def incomplete_name(person): + first_name = person.first_name + if not person.first_name and person.initials: + first_name = person.initials + if person.last_name_prefix: + return ' '.join([first_name, person.last_name_prefix, person.last_name]) + else: + return ' '.join([first_name, person.last_name]) + + +def create_manual_batch_for_pre_sepa_transactions_forwards(apps, scheme_editor): + Person = apps.get_model('members', 'Person') + Transaction = apps.get_model('personal_tab', 'Transaction') + ManualPaymentSettlement = apps.get_model('personal_tab', 'ManualPaymentSettlement') + SettlementManualPaymentTransaction = apps.get_model('personal_tab', 'SettlementManualPaymentTransaction') + PaymentMethod = apps.get_model('personal_tab', 'PaymentMethod') + logger.info(f'Create manual payment settlements for any transactions from before the SEPA debt collections came into effect....') + try: + pre_sepa_payment_method = PaymentMethod.objects.get(name_en='Pre-SEPA') + logger.info(f'Using existing Pre-SEPA payment method with PK "{pre_sepa_payment_method.pk}"...') + except PaymentMethod.DoesNotExist: + # No Pre-SEPA payment type exists, but we need one to link the new manual payment settlement to. Create one. + pre_sepa_payment_method = PaymentMethod.objects.create( + name_en="Pre-SEPA", + name_nl="Pre-SEPA", + description_en="Pre-SEPA payment. This payment was made before the introduction of SEPA authorizations.", + description_nl="Pre-SEPA-betaling. Deze betaling is gedaan voor de introductie van SEPA-machtigingen.", + visible=False, visible_memberships=False, visible_activities=False + ) + logger.info(f'No Pre-SEPA payment type existed. Created a new one with PK "{pre_sepa_payment_method.pk}"...') + + # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET + begin_date = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) + transaction_date = datetime.datetime(2013, 10, 30, 22, 59, 59, tzinfo=tz.utc) + # Get all the pre-SEPA transactions that have not been settled + pre_sepa_transactions = Transaction.objects.filter(settlement=None, date__lt=begin_date) + + # Group by person + people = pre_sepa_transactions.filter(person__isnull=False).order_by('person').distinct().values('person') + people_count = len(people) + for i, p in enumerate(people): + person = Person.objects.get(id=p['person']) + # Translate any description strings to the user's preferred language + with translation.override(person.preferred_language): + person_name = incomplete_name(person) + transactions = pre_sepa_transactions.filter(person=person) + logger.info(f'- [{i+1}/{people_count}] {person_name} - {transactions.count()} pre-SEPA transactions...') + + # Create a settlement payment to mark the old transactions as paid on the SEPA introduction date. + description = _("Settlement for transactions handled before SEPA debits were introduced - {person_name}").format(person_name=person_name) + create_settlement_for_transactions( + ManualPaymentSettlement, + SettlementManualPaymentTransaction, + transactions=transactions, + payment_method=pre_sepa_payment_method, + person=person, + description=description, + payment_datetime=transaction_date, + created_by=None + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0023_documenso_id_signed_document'), + ('personal_tab', '0013_authorization_documenso_id_and_more'), + ] + + operations = [ + # Create the new PaymentMethod model. + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=20, unique=True, verbose_name='Name (EN)')), + ('name_nl', models.CharField(max_length=20, unique=True, verbose_name='Name (NL)')), + ('description_en', models.TextField(verbose_name='Description (EN)')), + ('description_nl', models.TextField(verbose_name='Description (NL)')), + ('visible', models.BooleanField(default=True, help_text="This payment method will be visible when creating a manual payment directly in someone's personal tab.", verbose_name='Visible')), + ('visible_activities', models.BooleanField(default=True, help_text='This payment method will be visible when privileged members enter a manual payment for an activity.', verbose_name='Visible for activities')), + ('visible_memberships', models.BooleanField(default=True, help_text='This payment method will be visible when privileged members enter a manual payment for a membership.', verbose_name='Visible for memberships')), + ('frontend_icon_name', models.CharField(blank=True, max_length=20, null=True, verbose_name='Icon name for frontend')), + ], + options={ + 'verbose_name': 'payment method', + 'verbose_name_plural': 'payment methods', + 'ordering': ['name_en', 'name_nl'] + }, + ), + + # Create PaymentMethod instances from existing PaymentType instances. + migrations.RunPython(import_payment_methods_forwards, import_payment_methods_backwards), + + # Create the new PersonalTabSettlement parent model. + migrations.CreateModel( + name='PersonalTabSettlement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(max_length=140, validators=[django.core.validators.RegexValidator(message='Only alphanumerical signs are allowed', regex="^[a-zA-Z0-9-?:().,\\'+ ]*$")], verbose_name='description')), + ('amount', models.DecimalField(decimal_places=2, max_digits=8, verbose_name='amount')), + ], + options={ + 'verbose_name': 'personal tab settlement', + 'verbose_name_plural': 'personal tab settlements', + 'ordering': ['pk'], + }, + ), + + # Create new transaction models to support manual settlements. + migrations.CreateModel( + name='SettlementExtraBalanceTransaction', + fields=[ + ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='personal_tab.transaction')), + ], + bases=('personal_tab.transaction',), + ), + migrations.CreateModel( + name='SettlementManualPaymentTransaction', + fields=[ + ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='personal_tab.transaction')), + ], + bases=('personal_tab.transaction',), + ), + + # Create the new ManualPaymentSettlement sibling model (no data to migrate) + migrations.CreateModel( + name='ManualPaymentSettlement', + fields=[ + ('personaltabsettlement_ptr', models.OneToOneField( + auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='personal_tab.personaltabsettlement') + ), + ('payment_date', models.DateField(verbose_name='payment date')), + ('person', models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + to='members.person', verbose_name='person') + ), + ('payment_method', models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to='personal_tab.paymentmethod', verbose_name='Payment method') + ), + ('created_by', models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='+', to='members.person', verbose_name='Created by') + ), + ('extra_balance_transaction', models.OneToOneField( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, + related_name='manual_settlement', to='personal_tab.settlementextrabalancetransaction') + ), + ('manual_payment_transaction', models.OneToOneField( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, + related_name='manual_settlement', to='personal_tab.settlementmanualpaymenttransaction') + ) + ], + options={ + 'verbose_name': 'manual payment settlement', + 'verbose_name_plural': 'manual payment settlements', + 'ordering': ['-payment_date', '-person', '-id'], + }, + bases=('personal_tab.personaltabsettlement',), + ), + + # Rename the old DebtCollectionInstruction so the new one can take its name. + migrations.RenameModel('DebtCollectionInstruction', 'OldDebtCollectionInstruction'), + + # 4. Create the new DebtCollectionInstruction inheriting from PersonalTabSettlement. + migrations.CreateModel( + name='DebtCollectionInstruction', + fields=[ + ('personaltabsettlement_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='personal_tab.personaltabsettlement')), + ('end_to_end_id', models.CharField(max_length=35, validators=[ + django.core.validators.RegexValidator(message='Only alphanumerical signs are allowed', + regex="^[a-zA-Z0-9-?:().,\\'+ ]*$")], + verbose_name='end-to-end-id')), + ('amendment', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='instruction', to='personal_tab.amendment', + verbose_name='amendment')), + ('authorization', + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instructions', + to='personal_tab.authorization', verbose_name='mandate')), + ('batch', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instructions', + to='personal_tab.debtcollectionbatch', verbose_name='batch')), + ], + options={ + 'verbose_name': 'direct withdrawal-instruction', + 'verbose_name_plural': 'direct withdrawal-instructions', + 'ordering': ['batch', 'authorization'], + }, + bases=('personal_tab.personaltabsettlement',), + ), + + # Move the data over, preserving the original IDs. + migrations.RunPython(migrate_debt_collection_instructions_forwards, migrate_debt_collection_instructions_backwards), + + # Re-point foreign keys on Transactions to point to PersonalTabSettlement. + migrations.AlterField( + model_name='transaction', + name='debt_collection', + field=models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='transactions', + to='personal_tab.personaltabsettlement', + verbose_name='Settlement' + ), + ), + + # Rename Transaction.debt_collection foreign key to Transaction.settlement + migrations.RenameField( + model_name='transaction', + old_name='debt_collection', + new_name='settlement', + ), + + # Re-point foreign keys on Reversals to point to the new DebtCollectionInstruction model. + migrations.AlterField( + model_name='reversal', + name='instruction', + field=models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name='reversal', + to='personal_tab.debtcollectioninstruction', + verbose_name='instruction' + ), + ), + + # Drop the old table. + migrations.DeleteModel('OldDebtCollectionInstruction'), + + # Create a manual payment settlement for any transactions before the SEPA debt collections came into effect. + # (2013-10-30, 23:00:00, tz.utc) + migrations.RunPython(create_manual_batch_for_pre_sepa_transactions_forwards), + ] diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 2e4099dd..baba3e94 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -1,28 +1,33 @@ # -*- coding: UTF-8 -*- import datetime +import decimal import logging -from io import BytesIO -from typing import Optional +from decimal import Decimal +from typing import Optional, Iterable, Tuple from io import BytesIO import json from django.conf import settings +from django.contrib import admin from django.core.files.base import ContentFile +from django.templatetags.static import static from django.urls import reverse from django.shortcuts import get_object_or_404 from django.core.validators import RegexValidator from django.db import models, transaction -from django.db.models import Q +from django.db.models import Q, Sum from django.db.models.signals import pre_save, post_save, post_delete -from django.utils import timezone -from django.utils.translation import get_language, gettext_lazy as _l, gettext +from django.utils import timezone, translation +from django.utils.safestring import mark_safe +from django.utils.translation import get_language, gettext as _, gettext_lazy as _l from documenso_sdk import EnvelopeDistributeResponse from localflavor.generic.models import BICField, IBANField from amelie.claudia.tools import verify_instance from amelie.members.models import Person, Membership, Committee from amelie.personal_tab.managers import AuthorizationManager, DebtCollectionInstructionManager +from amelie.tools.logic import current_association_year class DiscountPeriod(models.Model): @@ -121,8 +126,8 @@ class Transaction(models.Model): price: The price that this transaction is for. person: The person this transaction is for. description: The description of this transaction. - discount: Links to the Discount object in which this credit is used. - debt_collection: Links to the Debt Collection Entry on which this transaction was collected. + discount: Links to the Discount object in which credit was used to pay for (part of) this transaction. + settlement: Links to the Personal Tab Settlement with which this transaction was settled. added_on: The date and time on which this object was created. added_by: The person that created this object. @@ -134,8 +139,8 @@ class Transaction(models.Model): description = models.CharField(max_length=200, blank=True, verbose_name=_l('Description')) discount = models.OneToOneField(Discount, blank=True, null=True, default=None, verbose_name=_l('discount'), on_delete=models.SET_NULL) - debt_collection = models.ForeignKey('DebtCollectionInstruction', verbose_name=_l('Direct withdrawal'), blank=True, null=True, - related_name="transactions", on_delete=models.PROTECT) + settlement = models.ForeignKey('PersonalTabSettlement', verbose_name=_l('Settlement'), blank=True, null=True, + related_name="transactions", on_delete=models.PROTECT) added_on = models.DateTimeField(verbose_name=_l('Added on'), auto_now_add=True) @@ -153,6 +158,37 @@ def get_absolute_url(self): def __str__(self): return '{} ({})'.format(self.description, self.price) + @property + def transaction_type(self): + if hasattr(self, 'activitytransaction'): + return "activitytransaction", _l("Activity Transaction") + elif hasattr(self, 'alexiatransaction'): + return "alexiatransaction", _l("Alexia Transaction") + elif hasattr(self, 'contributiontransaction'): + return "contributiontransaction", _l("Contribution Transaction") + elif hasattr(self, 'cookiecornertransaction'): + return "cookiecornertransaction", _l("Cookie Corner Transaction") + elif hasattr(self, 'customtransaction'): + return "customtransaction", _l("Custom Transaction") + elif hasattr(self, 'debtcollectiontransaction'): + return "debtcollectiontransaction", _l("Debt Collection Transaction") + elif hasattr(self, 'reversaltransaction'): + return "reversaltransaction", _l("Reversal Transaction") + elif hasattr(self, 'settlementextrabalancetransaction'): + return "settlementextrabalancetransaction", _l("Extra Balance Transaction") + elif hasattr(self, 'settlementmanualpaymenttransaction'): + return "settlementmanualpaymenttransaction", _l("Manual Payment Transaction") + else: + return None + + @property + def price_cents(self): + return (self.price * 100).quantize(Decimal("0"), decimal.ROUND_HALF_UP) + + def is_paid(self): + # A transaction is paid if a settlement exists, and that settlement is also paid. + return self.settlement.is_paid() if self.settlement else False + @property def editable(self): return False @@ -166,7 +202,7 @@ class CustomTransaction(Transaction): """ @property def editable(self): - return not self.debt_collection + return not self.settlement class ActivityTransaction(Transaction): @@ -218,7 +254,7 @@ def get_absolute_url(self): @property def editable(self): - return not self.debt_collection + return not self.settlement class AlexiaTransaction(Transaction): @@ -633,10 +669,10 @@ def send_signature_request(self) -> Optional[EnvelopeDistributeResponse]: def process_signed_document(self): if not self.documenso_id: - raise ValueError(gettext("Documenso ID is not set. No documents are due to be signed.")) + raise ValueError(_l("Documenso ID is not set. No documents are due to be signed.")) # Retrieve the document and save it to the database from amelie.tools.documenso import retrieve_documents - documents, _ = retrieve_documents(self.documenso_id) + documents, unused = retrieve_documents(self.documenso_id) if len(documents) > 1: logging.getLogger(__name__).warning(f"Received multiple documents from Documenso for {self}. Only saving the first one.") @@ -805,7 +841,59 @@ class Meta: verbose_name_plural = _l('direct withdrawal-batches') -class DebtCollectionInstruction(models.Model): +class PersonalTabSettlement(models.Model): + """ + Base class to represent a payment to settle the personal tab balance. + + This can either represent a DebtCollectionInstruction, or a ManualPaymentSettlement. + + Please note that processing properties of this model may be subject to privacy regulations. Refer to + https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. + """ + """Description rule.""" + description = models.CharField(max_length=140, verbose_name=_l('description'), validators=[SEPA_CHAR_VALIDATOR]) + + """Amount of the settlement. (Either to be collected via debit, or paid manually.)""" + amount = models.DecimalField(max_digits=8, decimal_places=2, verbose_name=_l('amount')) + + @property + def settlement_type(self): + if hasattr(self, 'debtcollectioninstruction'): + return self.debtcollectioninstruction.settlement_type + elif hasattr(self, 'manualpaymentsettlement'): + return self.manualpaymentsettlement.settlement_type + raise NotImplementedError("Base class Personal tab settlement has no settlement type.") + + def is_paid(self): + if hasattr(self, 'debtcollectioninstruction'): + return self.debtcollectioninstruction.is_paid() + elif hasattr(self, 'manualpaymentsettlement'): + return self.manualpaymentsettlement.is_paid() + raise NotImplementedError("Base class Personal tab settlement cannot be paid.") + + def __str__(self): + if hasattr(self, 'debtcollectioninstruction'): + return self.debtcollectioninstruction.__str__() + elif hasattr(self, 'manualpaymentsettlement'): + return self.manualpaymentsettlement.__str__() + if self.id: + return f'Personal tab settlement #{self.id}' + return 'Personal tab settlement (new)' + + def get_absolute_url(self): + if hasattr(self, 'debtcollectioninstruction'): + return self.debtcollectioninstruction.get_absolute_url() + elif hasattr(self, 'manualpaymentsettlement'): + return self.manualpaymentsettlement.get_absolute_url() + raise NotImplementedError("Base class Personal tab settlement has no detail view.") + + class Meta: + ordering = ['pk'] + verbose_name = _l('personal tab settlement') + verbose_name_plural = _l('personal tab settlements') + + +class DebtCollectionInstruction(PersonalTabSettlement): """ Instruction to collect some amount from a certain account. @@ -829,22 +917,25 @@ class DebtCollectionInstruction(models.Model): """End-to-end-id. This will be passed on to the cashed (the person paying).""" end_to_end_id = models.CharField(max_length=35, verbose_name=_l('end-to-end-id'), validators=[SEPA_CHAR_VALIDATOR]) - """Description rule.""" - description = models.CharField(max_length=140, verbose_name=_l('description'), validators=[SEPA_CHAR_VALIDATOR]) - - """Amount to be collected.""" - amount = models.DecimalField(max_digits=8, decimal_places=2, verbose_name=_l('amount')) - """An amendment given with this instruction.""" amendment = models.OneToOneField(Amendment, related_name='instruction', on_delete=models.PROTECT, verbose_name=_l('amendment'), null=True, blank=True) objects = DebtCollectionInstructionManager() + @property + def settlement_type(self): + return "INSTR" + + def is_paid(self): + # A debt collection is paid if there is no reversal for it, or if the reversal is also paid. + return self.reversal.is_paid() if hasattr(self, 'reversal') else True + def debt_collection_reference(self): - """Returns the debt collection reference with prefix.""" + """Returns the debt collection reference with a prefix.""" if self.id: return '%s%08d' % (DebtCollectionInstruction.PREFIX, self.id) + return None def __str__(self): return self.debt_collection_reference() or 'New debt collection instruction' @@ -858,6 +949,277 @@ class Meta: verbose_name_plural = _l('direct withdrawal-instructions') +class PaymentMethod(models.Model): + """ + Payment method used for a manual payment. + """ + name_nl = models.CharField(max_length=20, unique=True, verbose_name=_l('Name (NL)')) + name_en = models.CharField(max_length=20, unique=True, verbose_name=_l('Name (EN)')) + description_nl = models.TextField(verbose_name=_l('Description (NL)')) + description_en = models.TextField(verbose_name=_l('Description (EN)')) + visible = models.BooleanField(default=True, verbose_name=_l('Visible'), help_text=_l("This payment method will be visible when creating a manual payment directly in someone's personal tab.")) + visible_memberships = models.BooleanField(default=True, verbose_name=_l('Visible for memberships'), help_text=_('This payment method will be visible when privileged members enter a manual payment for a membership.')) + visible_activities = models.BooleanField(default=True, verbose_name=_l('Visible for activities'), help_text=_('This payment method will be visible when privileged members enter a manual payment for an activity.')) + frontend_icon_name = models.CharField(max_length=20, blank=True, null=True, verbose_name=_l('Icon name for frontend')) + + class Meta(object): + ordering = ['name_en', 'name_nl'] + verbose_name = _l('payment method') + verbose_name_plural = _l('payment methods') + + def __str__(self): + return f"{self.name} - {self.description}" + + @property + def name(self): + language = get_language() + + if language == "en" and self.name_en: + return self.name_en + else: + return self.name_nl + + @property + def description(self): + language = get_language() + + if language == "en" and self.description_en: + return self.description_en + else: + return self.description_nl + + @property + def icon_name(self): + return self.frontend_icon_name or 'wand' + + @admin.display(description=_l("Frontend Icon")) + @mark_safe + def frontend_icon_display(self): + if self.frontend_icon_name: + static_path = static(f"img/icons/{self.frontend_icon_name}.png") + return f'{self.frontend_icon_name}' + return "—" + + +class SettlementManualPaymentTransaction(Transaction): + """ + Transaction for an executed manual payment or refund of some amount to/from the personal tab balance. + + Please note that processing properties of this model may be subject to privacy regulations. Refer to + https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. + """ + pass + + +class SettlementExtraBalanceTransaction(Transaction): + """ + Transaction representing any extra balance that was left over after a manual settlement. + Can only be created from extra deposited money in a ManualPaymentSettlement. + + Please note that processing properties of this model may be subject to privacy regulations. Refer to + https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. + """ + pass + + +class ManualPaymentSettlement(PersonalTabSettlement): + """ + Record of a manual payment to settle a personal tab balance. + + Please note that processing properties of this model may be subject to privacy regulations. Refer to + https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. + """ + + """Prefix for payment reference.""" + PREFIX = 'IA-MANUAL-' + + """The date on which this manualpayment was made.""" + payment_date = models.DateField(verbose_name=_l('payment date')) + + """Person that the settlement relates to.""" + person = models.ForeignKey(Person, verbose_name=_l('person'), null=True, blank=True, on_delete=models.PROTECT) + + """Payment method""" + payment_method = models.ForeignKey(PaymentMethod, verbose_name=_l('Payment method'), on_delete=models.PROTECT) + + """Person that created the manual payment settlement.""" + created_by = models.ForeignKey(Person, verbose_name=_l('Created by'), blank=True, null=True, related_name="+", on_delete=models.PROTECT) + # '+' related_name makes sure no reverse relation is added to Person + + """The ManualPaymentTransaction that was created in this settlement.""" + manual_payment_transaction = models.OneToOneField(SettlementManualPaymentTransaction, on_delete=models.RESTRICT, related_name='manual_settlement', blank=True, null=True) + + """The ManualPaymentTransaction that was created in this settlement.""" + extra_balance_transaction = models.OneToOneField(SettlementExtraBalanceTransaction, on_delete=models.RESTRICT, related_name='manual_settlement', blank=True, null=True) + + class Meta: + ordering = ['-payment_date', '-person', '-id'] + verbose_name = _l('manual payment settlement') + verbose_name_plural = _l('manual payment settlements') + + @property + def settlement_type(self): + return "MANUAL" + + def is_paid(self): + # A manual payment settlement is considered to always be paid when it is created. + return True + + def payment_reference(self): + """Returns the payment reference with a prefix.""" + if self.id: + return '%s%08d' % (ManualPaymentSettlement.PREFIX, self.id) + return None + + def __str__(self): + return self.payment_reference() or 'New manual payment settlement' + + def get_absolute_url(self): + return reverse('personal_tab:manual_payment_settlement_view', args=(), kwargs={'id': self.id, }) + + def deletion_possible(self) -> Tuple[bool, str]: + """ + Deletion is possible if: + - The settlement payment date falls in the current association year + - The SettlementExtraBalanceTransaction is not settled in any other settlement. + + - The sum of the transactions (excluding the SettlementManualPaymentTransaction) should equal negative the SettlementManualPaymentTransaction price. + - The settlement amount should equal the SettlementManualPaymentTransaction price plus the SettlementExtraBalanceTransaction price. + """ + # General limitation to avoid creating inconsistencies in closed book years. + # - The settlement was created in the current association year + if current_association_year(dt=self.payment_date) != current_association_year(): + return False, _("Manual settlements can only be deleted in the same association year as they were paid in.") + + # Get some info for the next few checks + manual_payment_transaction = self.manual_payment_transaction + manual_payment_transaction_price = manual_payment_transaction.price if manual_payment_transaction else Decimal("0.00") + extra_balance_transaction = self.extra_balance_transaction + extra_balance_transaction_price = extra_balance_transaction.price if extra_balance_transaction else Decimal("0.00") + transactions = self.transactions.all() + if manual_payment_transaction: + transactions = transactions.exclude(pk=manual_payment_transaction.pk) + transactions_sum = (transactions.aggregate(total=Sum('price'))['total'] or Decimal("0.00")) + + # General limitation to avoid creating inconsistencies in other settlements. + # - The SettlementExtraBalanceTransaction is not settled in any other settlement. + if extra_balance_transaction and extra_balance_transaction.settlement is not None: + return False, _("The extra balance that was created in this settlement was already used elsewhere.") + + # Sanity checks to avoid creating an inconsistent state after deletion (in the case of any weird settlements): + # - The sum of the transactions (excluding the SettlementManualPaymentTransaction) should equal negative the SettlementManualPaymentTransaction price. + if transactions_sum != -manual_payment_transaction_price: + return False, _("The total sum of the transactions ({transactions_sum}) does not inversely equal the linked manual payment's amount ({manual_amount}).").format( + transactions_sum=transactions_sum, manual_amount=manual_payment_transaction_price, + ) + # - The settlement amount should equal the SettlementManualPaymentTransaction price plus the SettlementExtraBalanceTransaction price. + if self.amount != -(manual_payment_transaction_price + extra_balance_transaction_price): + return False, _("The settlement's amount ({settlement_amount}) does not inversely equal the manual payment's amount and the extra balance ({manual_amount} + {extra_balance} = {total}).").format( + settlement_amount=self.amount, + manual_amount=manual_payment_transaction_price, + extra_balance=extra_balance_transaction_price, + total=(manual_payment_transaction_price + extra_balance_transaction_price) + ) + + return True, "" + + @classmethod + def create_for_transactions( + cls, transactions: Iterable[Transaction], payment_method: PaymentMethod, person: Person, + settlement_description: str, payment_description: Optional[str] = None, leftover_balance_description: Optional[str] = None, + paid_amount: Optional[Decimal] = None, payment_datetime: Optional[datetime.datetime] = None, + created_by: Optional[Person] = None + ) -> Optional['ManualPaymentSettlement']: + """ + Create a manual personal tab settlement. + + :param transactions: The list of transactions that will be settled by this settlement. Can be an empty list. + :param payment_method: The PaymentMethod that was used for the payment. + :param person: The Person that this settlement is for. + :param settlement_description: A description used for the settlement as a whole. + :param paid_amount: Optional. The total amount paid by the person (as Decimal) (can be negative for a refund + or 0 for an internal settlement with the personal tab balance). + Will equal the sum of transactions if None. + :param payment_description: Optional. The description that is used for the transaction representing the amount paid. + :param leftover_balance_description: Optional. The description that is used for the transaction representing + any leftover (extra) balance after settling the transactions. + :param payment_datetime: The datetime of the settlement and transactions (current datetime is used if None). + :param created_by: Optional. The person that created the settlement, for auditing reasons. + :return: The ManualPaymentSettlement object that was created or `None` if no object was created. + """ + with transaction.atomic(): + # Get the current datetime if none was given + if payment_datetime is None: + payment_datetime = timezone.now() + + # If no transactions are given and no paid amount is given or the paid amount is 0, the settlement is useless. Cancel creation. + if not transactions and (paid_amount is None or paid_amount == 0): + return None + + # Calculate the total settlement amount + total_transaction_price = sum(t.price for t in transactions) or Decimal("0.00") + if paid_amount is None: + paid_amount = -total_transaction_price + leftover_balance_amount = total_transaction_price - paid_amount + + # Get the default descriptions if none were given + payment_date = payment_datetime.date() + with translation.override(person.preferred_language): + if payment_description is None: + if paid_amount > 0: + payment_description = _("Deposit to personal tab balance on {date} for {name}").format( + date=payment_date, name=person.incomplete_name() + ) + else: + payment_description = _("Refund of personal tab balance on {date} for {name}").format( + date=payment_date, name=person.incomplete_name() + ) + if leftover_balance_description is None: + leftover_balance_description = _("Leftover balance after settlement on {date} for {name}").format( + date=payment_date, name=person.incomplete_name() + ) + + # Create a ManualPayment settlement with the given payment method. + settlement = ManualPaymentSettlement.objects.create( + payment_date=payment_datetime.date(), + payment_method=payment_method, + description=settlement_description[:140], + person=person, + created_by=created_by, + amount=paid_amount, + ) + + # Save the settlement reference in all transactions + for t in transactions: + t.settlement = settlement + t.save() + + # If transactions were settled, create a SettlementManualPaymentTransaction to + # register the amount of money added/removed from the balance. + if total_transaction_price != 0: + payment_transaction = SettlementManualPaymentTransaction( + date=payment_datetime, price=-total_transaction_price, person=person, + description=payment_description[:200], settlement=settlement + ) + payment_transaction.save() + settlement.manual_payment_transaction = payment_transaction + settlement.save() + + # If there is an amount of leftover balance that needs to be kept, create a + # SettlementExtraBalanceTransaction to register the open balance as an unpaid transaction. + if leftover_balance_amount != 0: + leftover_balance_transaction = SettlementExtraBalanceTransaction( + date=payment_datetime, price=leftover_balance_amount, person=person, + description=leftover_balance_description[:200], + settlement=None # Leaves it as unpaid so it can be used. + ) + leftover_balance_transaction.save() + settlement.extra_balance_transaction = leftover_balance_transaction + settlement.save() + + return settlement + + class Reversal(models.Model): """ Reversal of an already collected amount. @@ -866,7 +1228,6 @@ class Reversal(models.Model): https://privacy.ia.utwente.nl/ and check whether processing the property is allowed for your purpose. """ - class ReversalReasons(models.TextChoices): """Possible reasons for reversals""" AC01 = 'AC01', 'AC01 - IncorrectAccountNumber' @@ -917,6 +1278,10 @@ class ReversalReasons(models.TextChoices): def __str__(self): return _l('Debit reversal %s') % self.instruction.debt_collection_reference() + def is_paid(self): + # A reversal is paid when the related ReversalTransaction is paid. + return self.transaction.is_paid() + class Meta: ordering = ['date'] verbose_name = _l('debit reversal') diff --git a/amelie/personal_tab/static/css/personal_tab_pos.css b/amelie/personal_tab/static/css/personal_tab_pos.css index f58e80e0..d1c95f49 100644 --- a/amelie/personal_tab/static/css/personal_tab_pos.css +++ b/amelie/personal_tab/static/css/personal_tab_pos.css @@ -120,7 +120,6 @@ html, body { /* Beta warning */ #beta-warning { display: block; - /*display: none; !* TODO: Remove *!*/ position: absolute; bottom: 6vh; right: 6vw; @@ -1457,4 +1456,4 @@ CALCULATOR .calculator-buttons .row a.add-calc-to-cart:hover { width: 26vh; background-color: var(--ia-green-dark); -} \ No newline at end of file +} diff --git a/amelie/personal_tab/statistics.py b/amelie/personal_tab/statistics.py index 2a8da3a9..c657faad 100644 --- a/amelie/personal_tab/statistics.py +++ b/amelie/personal_tab/statistics.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from django.db import connection from django.db.models import Count, Sum from django.db.models.functions import TruncDay @@ -30,29 +32,29 @@ def statistics_cookie_corner_breakdown_for_ledger(start, end): for article_aggregation in articles_aggregator: if article_aggregation['amount__sum']: - article_sum = article_aggregation['price__sum'] - article_discount_sum = article_aggregation['discount__amount__sum'] + article_sum = article_aggregation['price__sum'] or Decimal("0.00") + article_discount_sum = article_aggregation['discount__amount__sum'] or Decimal("0.00") rows.append({ 'article': all_articles[article_aggregation['article']], - 'count': article_aggregation['amount__sum'], + 'count': article_aggregation['amount__sum'] or 0, 'discount': article_discount_sum, 'sum': article_sum, - 'total': article_sum + (article_discount_sum or 0) + 'total': article_sum + article_discount_sum }) # Total row ledger_aggregation = cookie_corner_transactions.aggregate( Sum('amount'), Sum('price'), Sum('discount__amount')) - ledger_sum = ledger_aggregation['price__sum'] or 0 - ledger_discount_sum = ledger_aggregation['discount__amount__sum'] + ledger_sum = ledger_aggregation['price__sum'] or Decimal("0.00") + ledger_discount_sum = ledger_aggregation['discount__amount__sum'] or Decimal("0.00") total = { 'count': ledger_aggregation['amount__sum'] or 0, 'discount': ledger_discount_sum, 'sum': ledger_sum, - 'total': ledger_sum + (ledger_discount_sum or 0) + 'total': ledger_sum + ledger_discount_sum } return {'rows': rows, 'total': total} @@ -71,8 +73,8 @@ def statistics_cookie_corner_breakdown(start, end): category_transactions = cookie_corner_transactions.filter(article__category_id=category) category_aggregation = category_transactions.aggregate(Sum('amount'), Sum('price'), Sum('discount__amount')) category_count = category_aggregation['amount__sum'] or 0 - category_sum = category_aggregation['price__sum'] or 0 - category_discount_sum = category_aggregation['discount__amount__sum'] or 0 + category_sum = category_aggregation['price__sum'] or Decimal("0.00") + category_discount_sum = category_aggregation['discount__amount__sum'] or Decimal("0.00") category_total = category_sum + category_discount_sum articles = category_transactions.order_by().distinct().values('article') @@ -84,10 +86,10 @@ def statistics_cookie_corner_breakdown(start, end): article_aggregation = category_transactions.filter(article=article).aggregate(Sum('amount'), Sum('price'), Sum('discount__amount')) - article_count = article_aggregation['amount__sum'] - article_sum = article_aggregation['price__sum'] - article_discount_sum = article_aggregation['discount__amount__sum'] - article_total = article_sum + (article_discount_sum or 0) + article_count = article_aggregation['amount__sum'] or 0 + article_sum = article_aggregation['price__sum'] or Decimal("0.00") + article_discount_sum = article_aggregation['discount__amount__sum'] or Decimal("0.00") + article_total = article_sum + article_discount_sum cat_rows.append({ 'article': article, @@ -122,8 +124,8 @@ def _statistics_cookie_corner_totals_category(cookie_corner_transactions, ledger transactions = cookie_corner_transactions.filter(article__ledger_account_id=ledger_account) totals = transactions.aggregate(Sum('amount'), Sum('price'), Sum('discount__amount')) amount = totals['amount__sum'] or 0 - paid = totals['price__sum'] or 0 - discount = totals['discount__amount__sum'] or 0 + paid = totals['price__sum'] or Decimal("0.00") + discount = totals['discount__amount__sum'] or Decimal("0.00") total = paid + discount return [LedgerAccount.objects.get(id=ledger_account), amount, discount, paid, total] @@ -131,7 +133,7 @@ def _statistics_cookie_corner_totals_category(cookie_corner_transactions, ledger def statistics_cookie_corner_total(start, end): totals = CookieCornerTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'), Sum('discount__amount')) - return (totals['price__sum'] or 0) + (totals['discount__amount__sum'] or 0) + return (totals['price__sum'] or Decimal("0.00")) + (totals['discount__amount__sum'] or Decimal("0.00")) def statistics_activities(start, end): @@ -140,7 +142,7 @@ def statistics_activities(start, end): number_of_transactions = activity_transactions.count() number_of_enrollments = activity_transactions.filter(participation__isnull=False).count() - total = activity_transactions.aggregate(Sum('price'))['price__sum'] or 0 + total = activity_transactions.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") # Do not use order, so that distinct works well. See Django manual. events = activity_transactions.order_by().distinct().values('event') @@ -154,7 +156,7 @@ def statistics_activities(start, end): event_transaction_count = activity_transactions.filter(event=event).count() event_participants = event.participants.count() if event else activity_transactions.filter(event=event, participation__isnull=False).count() - event_transaction_sum = activity_transactions.filter(event=event).aggregate(Sum('price'))['price__sum'] + event_transaction_sum = activity_transactions.filter(event=event).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") rows.append({ 'event': event, 'count': event_transaction_count, @@ -165,12 +167,12 @@ def statistics_activities(start, end): def statistics_activities_total(start, end): - return ActivityTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'))['price__sum'] or 0 + return ActivityTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") def statistics_contribution_transactions(start, end): contribution_transactions = ContributionTransaction.objects.filter(date__gte=start, date__lt=end) - contribution_sum = contribution_transactions.aggregate(Sum('price'))['price__sum'] or 0 + contribution_sum = contribution_transactions.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") # Do not use order, so that values works well. See Django manual. sum_per_membership_type = contribution_transactions.order_by().values('membership__type').annotate(Sum('price')) @@ -179,11 +181,11 @@ def statistics_contribution_transactions(start, end): # Count positive and negative transactions positive_transactions_per_membership_type = contribution_transactions.filter(price__gt=0).order_by().values( 'membership__type').annotate(Count('price')) - positive_transactions_per_membership_type_dict = {x['membership__type']: x['price__count'] + positive_transactions_per_membership_type_dict = {x['membership__type']: x['price__count'] or 0 for x in positive_transactions_per_membership_type} negative_transactions_per_membership_type = contribution_transactions.filter(price__lt=0).order_by().values( 'membership__type').annotate(Count('price')) - negative_transactions_per_membership_type_dict = {x['membership__type']: x['price__count'] + negative_transactions_per_membership_type_dict = {x['membership__type']: x['price__count'] or 0 for x in negative_transactions_per_membership_type} types = MembershipType.objects.filter(pk__in=sum_per_membership_type_dict.keys()) @@ -204,12 +206,12 @@ def statistics_contribution_transactions(start, end): def statistics_contribution_transactions_total(start, end): return ContributionTransaction.objects.filter(date__gte=start, date__lt=end).aggregate( - Sum('price'))['price__sum'] or 0 + Sum('price'))['price__sum'] or Decimal("0.00") def statistics_discount_periods(start, end): discount_transactions = Discount.objects.filter(date__gte=start, date__lt=end) - discount_sum = discount_transactions.aggregate(Sum('amount'))['amount__sum'] or 0 + discount_sum = discount_transactions.aggregate(Sum('amount'))['amount__sum'] or Decimal("0.00") discount_aggregated = discount_transactions.order_by().values('discount_period').annotate( Count('amount'), Sum('amount')).order_by('discount_period') @@ -218,8 +220,8 @@ def statistics_discount_periods(start, end): for discount in discount_aggregated: discount_period = DiscountPeriod.objects.get(id=discount['discount_period']) - discount_count = discount['amount__count'] - discount_sum = discount['amount__sum'] + discount_count = discount['amount__count'] or 0 + discount_sum = discount['amount__sum'] or Decimal("0.00") rows.append({ 'discount_period': discount_period, @@ -233,32 +235,35 @@ def statistics_discount_periods(start, end): def statistics_other_transactions(start, end): rows = CustomTransaction.objects.filter(date__gte=start, date__lt=end).select_related('person') count = rows.count() - custom_sum = rows.aggregate(Sum('price'))['price__sum'] or 0 + custom_sum = rows.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") return {'rows': rows, 'count': count, 'sum': custom_sum} def statistics_other_transactions_total(start, end): - return CustomTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'))['price__sum'] or 0 + return CustomTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") def statistics_discount_credits(start, end): transactions = DiscountCredit.objects.filter(date__gte=start, date__lt=end, discount__isnull=True) credit_count = transactions.count() - credit_sum = transactions.aggregate(Sum('price'))['price__sum'] or 0 + credit_sum = transactions.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") credit_aggregated = transactions.order_by().values('discount_period').annotate( Count('price'), Sum('price')).order_by('discount_period') rows = [] for credit in credit_aggregated: discount_period = DiscountPeriod.objects.get(id=credit['discount_period']) - row = {'discount_period': discount_period, 'count': credit['price__count'], 'sum': credit['price__sum'], } - rows.append(row) + rows.append({ + 'discount_period': discount_period, + 'count': credit['price__count'] or 0, + 'sum': credit['price__sum'] or Decimal("0.00") + }) return {'count': credit_count, 'rows': rows, 'sum': credit_sum} def statistics_alexia_transactions(start, end): alexia_transactions = AlexiaTransaction.objects.filter(date__gte=start, date__lt=end) - alexia_sum = alexia_transactions.aggregate(Sum('price'))['price__sum'] or 0 + alexia_sum = alexia_transactions.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") # https://stackoverflow.com/questions/8746014/django-group-sales-by-month alexia_rows = alexia_transactions.annotate(day=TruncDay('date')).values('day', 'description').annotate( @@ -267,7 +272,7 @@ def statistics_alexia_transactions(start, end): def statistics_alexia_transactions_total(start, end): - return AlexiaTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'))['price__sum'] or 0 + return AlexiaTransaction.objects.filter(date__gte=start, date__lt=end).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") def statistics_functions_ledgers(): diff --git a/amelie/personal_tab/templates/authorization/list.html b/amelie/personal_tab/templates/authorization/list.html index 591bbfcd..7bc594bd 100644 --- a/amelie/personal_tab/templates/authorization/list.html +++ b/amelie/personal_tab/templates/authorization/list.html @@ -88,6 +88,11 @@

{% trans 'Mandates' %}

{{ authorization.person }} +   + + + {% else %} — {% endif %} diff --git a/amelie/personal_tab/templates/authorization/view.html b/amelie/personal_tab/templates/authorization/view.html index 6aef7a9e..eca8bae4 100644 --- a/amelie/personal_tab/templates/authorization/view.html +++ b/amelie/personal_tab/templates/authorization/view.html @@ -24,6 +24,11 @@

{% trans 'Mandate' %}

{{ authorization.person }} +   + + + {% else %} — {% endif %} @@ -92,7 +97,7 @@

{% trans 'Amendments' %}

{% endif %} {% if authorization.next_amendment %} - + {% if amendment == authorization.next_amendment %} {% trans 'Edit' %}
{% trans 'Delete' %}
diff --git a/amelie/personal_tab/templates/balance/balance.html b/amelie/personal_tab/templates/balance/balance.html index 18394bc5..4ddc8f00 100644 --- a/amelie/personal_tab/templates/balance/balance.html +++ b/amelie/personal_tab/templates/balance/balance.html @@ -40,7 +40,16 @@

{% trans 'Personal tab' %}

{% for former_member, total in former_member_totals %} - {{ former_member }} + + + {{ former_member }} + +   + + + + {{ total|stringformat:".2f" }} {% endfor %} @@ -64,7 +73,16 @@

{% trans 'Personal tab' %}

{% for member, total in member_totals %} - {{ member }} + + + {{ member }} + +   + + + + {{ total|stringformat:".2f" }} {% endfor %} @@ -94,7 +112,14 @@

{% trans 'Exam credits' %}

{% for former_member, total in exam_cookie_former_member_totals %} - {{ former_member }} + + {{ former_member }} + +   + + + {{ total|stringformat:".2f" }} @@ -120,7 +145,14 @@

{% trans 'Exam credits' %}

{% for member, total in exam_cookie_member_totals %} - {{ member }} + + {{ member }} + +   + + + {{ total|stringformat:".2f" }} diff --git a/amelie/personal_tab/templates/cookie_corner_dashboard.html b/amelie/personal_tab/templates/cookie_corner_dashboard.html index 43486cf4..99f6df17 100644 --- a/amelie/personal_tab/templates/cookie_corner_dashboard.html +++ b/amelie/personal_tab/templates/cookie_corner_dashboard.html @@ -10,7 +10,15 @@

-

{% blocktrans %}Your current balance is € {{ curr_balance }}. Including future transactions that will be € {{ all_balance }}.{% endblocktrans %}

+

{% blocktrans %} + Your current balance is € {{ curr_balance }}. Including future transactions that will be € {{ all_balance }}. + {% endblocktrans %}

+ +

{% blocktrans %} + If you signed a direct debit mandate, the balance is usually settled by withdrawing it from your bank account periodically. + If you do not have a mandate, you can settle any outstanding balance with a manual payment. To arrange such a payment, please contact the board. + {% endblocktrans %}

+ {% for instruction in future_debt_collection_instructions %}

{% blocktrans with date=instruction.batch.execution_date amount=instruction.amount url=instruction.get_absolute_url %} @@ -48,6 +56,9 @@

{% trans 'Previous transactions' %} + + {% trans 'Unpaid transactions' %} + {% trans 'History of exam credit' %} @@ -55,25 +66,26 @@

{% trans 'Cookie Corner Wrapped' %} {{ wrapped_year }} {% endif %} -

- {% if request.is_board %} - - {% endif %} + {% if request.is_board %} + + {% endif %} +

{% if person and not date_from %} - {% include "info/debt_collection_instructions.html" %} + {% include "info/payments.html" %} {% include "info/account_information.html" %}
diff --git a/amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html b/amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html new file mode 100644 index 00000000..bd962a40 --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html @@ -0,0 +1,98 @@ +{% extends "basis.html" %} +{% load i18n %} + +{% block titel %}{% trans 'Manual Payment Settlement' %}{% endblock titel %} + +{% block content %} +
+

{% trans 'Manual Payment Settlement' %}

+
+
+ + + + + + + {% if request.is_board %} + + + + + {% endif %} + + + + + + + + + + + + + + + + + {% if request.is_board %} + + + + + {% endif %} + +
{% trans 'Reference of manual payment' %}{{ settlement.payment_reference }}
{% trans 'Person' %} + {% if settlement.person %} + + {{ settlement.person }} + +   + + + + {% else %} + — + {% endif %} +
{% trans 'Date of payment' %}{{ settlement.payment_date|date:"d F Y" }}
{% trans 'Method of payment' %} + + {{ settlement.payment_method.name }}
+ {{ settlement.payment_method.description }} +
{% trans 'Description' %}{{ settlement.description }}
{% trans 'Price' %}€ {{ settlement.amount }}
{% trans 'Created by' %} + {% if settlement.created_by %} + + {{ settlement.created_by }} + + {% else %} + — + {% endif %} +
+
+ + {% if request.is_board %} + {% with deletion_possible=settlement.deletion_possible %} + {% if deletion_possible.0 %} + + {% trans 'Delete manual payment' %} + + {% else %} + + {% trans 'Delete manual payment' %} + + {% endif %} + {% endwith %} + {% endif %} +
+
+ +
+

{% trans 'Transactions' %}

+
+ {% include 'lists/transactions_categorized.html' with transactions=transactions total=show_total totals=totals hide_kcal=True %} +
+
+{% endblock content %} diff --git a/amelie/personal_tab/templates/cookie_corner_manual_payments_list.html b/amelie/personal_tab/templates/cookie_corner_manual_payments_list.html new file mode 100644 index 00000000..a712f152 --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_manual_payments_list.html @@ -0,0 +1,84 @@ +{% extends "basis.html" %} +{% load i18n %} + +{% block titel %}{% blocktrans with page=page_obj.number total=page_obj.paginator.num_pages %}Manual payments - Page {{ page }}/{{ total }}{% endblocktrans %}{% endblock titel %} + +{% block content %} +
+

{% blocktrans with page=page_obj.number total=page_obj.paginator.num_pages %}Manual payments - Page {{ page }}/{{ total }}{% endblocktrans %}

+
+ +

+ {% blocktrans %} + These are payments that people made manually directly to the association, for example, by cash or by PIN. + A manual payment can pay for transactions that were made against their personal tab, or it can add + a negative amount to their personal tab to create a credit they can use for activities. + {% endblocktrans %} +

+

+ {% blocktrans %} + Click on a payment description to view details about which transactions the manual payment paid for, + and if there was any extra money deposited. + {% endblocktrans %} +

+ + +

+ {% blocktrans %} + To create a manual payment, go to the person's personal tab overview + and click on the "Add manual payment" button in the relevant block. + {% endblocktrans %} +

+ + {% include 'paginator.html' with page=page_obj %} + + {% if page_obj.object_list %} +
+ + + + + + + + + + + + {% for manual_payment in page_obj.object_list.select_related %} + + + + + + + + {% endfor %} + +
{% trans 'Date' %}{% trans 'Person' %}{% trans 'Description' %}{% trans 'Payment method' %}{% trans 'Price' %}
+ {{ manual_payment.payment_date }} + + + {{ manual_payment.person }} + +   + + + + + + {{ manual_payment.description }} + + + {{ manual_payment.payment_method.name }} + € {{ manual_payment.amount }}
+
+ {% else %} +

{% trans "No manual payments have been found." %}

+ {% endif %} + + {% include 'paginator.html' with page=page_obj %} +
+
+{% endblock content %} diff --git a/amelie/personal_tab/templates/cookie_corner_overview.html b/amelie/personal_tab/templates/cookie_corner_overview.html index 8ec23498..f5c4cb9c 100644 --- a/amelie/personal_tab/templates/cookie_corner_overview.html +++ b/amelie/personal_tab/templates/cookie_corner_overview.html @@ -25,7 +25,9 @@

{% trans 'Digital cookie and candy list' %}

  • {% trans 'Direct withdrawal-tasks (SEPA)' %}
  • +
  • {% trans 'Manual payments' %}
  • {% trans 'Unpaid memberships' %}
  • +
  • {% trans 'Unpaid transactions' %}
  • diff --git a/amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html b/amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html new file mode 100644 index 00000000..3559796a --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html @@ -0,0 +1,26 @@ +{% extends "basis.html" %} +{% load i18n %} + +{% block titel %}{% trans 'Delete manual payment' %}{% endblock titel %} + +{% block content %} +
    +

    {% blocktrans %}Delete manual payment '{{ object }}'{% endblocktrans %}

    + +
    +

    {% blocktrans with person=object.person %} + Are you sure you want to delete '{{ object }}' for {{ person }}? + {% endblocktrans %}

    + + + {% trans 'Cancel' %} + + +
    + {% csrf_token %} + +
    +
    +
    + +{% endblock content %} diff --git a/amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html b/amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html new file mode 100644 index 00000000..6b30dd04 --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html @@ -0,0 +1,203 @@ +{% extends "basis.html" %} +{% load i18n %} + +{% block titel %}{% blocktrans %}Create manual payment for {{ person }}{% endblocktrans %}{% endblock titel %} + +{% block content %} +
    +
    +

    {% blocktrans %}New manual payment for {{ person }}{% endblocktrans %}

    +
    + {% if not object %} +

    + {% if transaction_type == 'cookie_corner' %} + {% trans 'You are now adding a transaction to a personal tab' %} + {% elif transaction_type == 'other' %} + {% trans 'You are now adding a transaction in the category "Other"' %} + {% endif %} +

    + {% endif %} + + {% csrf_token %} + {{ form }} + +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    {% trans 'Total of selected' %}{% trans 'Amount being paid' %}{% trans 'Personal tab balance before payment' %}{% trans 'Personal tab balance after payment' %}
    {% trans 'Today' %}{% trans 'Including future' %}{% trans 'Today' %}{% trans 'Including future' %}
    € 0.00€ € 0.00€ 0.00€ 0.00€ 0.00
    +
    +
    +

    {% blocktrans %}Unpaid transactions{% endblocktrans %}

    +
    +

    {% blocktrans %} + Please select the transactions that will be marked as paid with this settlement. + {% endblocktrans %}

    +

    {% blocktrans %} + Rows marked in green are transactions that have a date in the future. + {% endblocktrans %}

    + {{ transaction_formset.management_form }} + + + {% if transaction_formset.non_form_errors or transaction_formset.total_error_count %} +

    {% trans 'There were errors while submitting the form.' %}

    + {% if formset.non_form_errors %} +
      + {% for error in transaction_formset.non_form_errors %} +
    • + {{ error }} +
    • + {% endfor %} +
    + {% if formset.total_error_count %} +

    {% trans 'For more errors, see below.' %}

    + {% endif %} + {% else %} +

    {% trans 'See below for the errors.' %}

    + {% endif %} + {% endif %} + + + + + + + + + + + + + {% for form in transaction_formset %} + + date_now %} style="background: rgba(0, 255, 0, 0.15)"{% endif %}> + + + + + + + {% empty %} + + + + {% endfor %} + +
    +
    + +
    {% trans '#ID' %}{% trans 'Date' %}{% trans 'Description' %}{% trans 'Price' %}
    + {% if form.errors %} + {{ form.include.errors }} + {% endif %} + {{ form.include }} + + {{ form.transaction.id }} + {{ form.transaction.date }} + + {{ form.transaction.description }} + + + € {{ form.transaction.price }} +
    {% trans 'No unpaid transactions found!' %}
    +
    +
    +
    + + {{ balance|json_script:"balance-data" }} + + + +{% endblock content %} diff --git a/amelie/personal_tab/templates/cookie_corner_person_manual_payments.html b/amelie/personal_tab/templates/cookie_corner_person_manual_payments.html new file mode 100644 index 00000000..ab8a4f63 --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_person_manual_payments.html @@ -0,0 +1,46 @@ +{% extends "basis.html" %} +{% load i18n %} + +{% block titel %}{% blocktrans %}Manual payments for {{ person }}{% endblocktrans %}{% endblock titel %} + +{% block content %} +
    +

    {% blocktrans %}Manual payments for {{ person }}{% endblocktrans %}

    +
    + {% if manual_payments %} +
    + + + + + + + + + + + {% for manual_payment in manual_payments.select_related %} + + + + + + + {% endfor %} + +
    {% trans 'Date' %}{% trans 'Description' %}{% trans 'Payment method' %}{% trans 'Price' %}
    + {{ manual_payment.payment_date }} + + + {{ manual_payment.description }} + + + {{ manual_payment.payment_method.name }} + € {{ manual_payment.amount }}
    +
    + {% else %} +

    {% trans "No manual payments have been found." %}

    + {% endif %} +
    +
    +{% endblock content %} diff --git a/amelie/personal_tab/templates/cookie_corner_transactions.html b/amelie/personal_tab/templates/cookie_corner_transactions.html index 6796fcb3..10bfc8b6 100644 --- a/amelie/personal_tab/templates/cookie_corner_transactions.html +++ b/amelie/personal_tab/templates/cookie_corner_transactions.html @@ -45,8 +45,14 @@

    {% trans 'Activities' %}

    {% trans 'Alexia' %}

    {% include "lists/alexia_transactions.html" with transactions=all_transactions.2 totals=totals.2 %} +

    {% trans 'Contribution' %}

    + {% include "lists/contribution_transactions.html" with transactions=all_transactions.3 totals=totals.3 %} +

    {% trans 'Other' %}

    - {% include "lists/custom_transactions.html" with transactions=all_transactions.3 totals=totals.3 %} + {% include "lists/custom_transactions.html" with transactions=all_transactions.4 totals=totals.4 %} + +

    {% trans 'Payments' %}

    + {% include "lists/payment_transactions.html" with transactions=payment_transactions totals=payment_totals %} {% else %} {% include "lists/transactions_totals.html" %} {% endif %} diff --git a/amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html b/amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html new file mode 100644 index 00000000..95f642b9 --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html @@ -0,0 +1,82 @@ +{% extends "basis.html" %} +{% load humanize %} +{% load i18n transaction_link %} + +{% block titel %}{% trans 'Unpaid Transactions' %}{% endblock titel %} + +{% block content %} +
    +

    {% blocktrans with name=person.incomplete_name %} + Unpaid Transactions for {{ name }} + {% endblocktrans %}

    + +
    +

    + {% blocktrans with curr_balance=curr_balance all_balance=all_balance %} + Your current balance is € {{ curr_balance }}. Including future transactions that will be € {{ all_balance }}. + {% endblocktrans %} + {% trans 'Back to overview' %} +

    + +

    {% trans 'Overview' %}

    +
    + + + + + + + + + {% if sums_per_type %} + {% for entry_name, entry_sum in sums_per_type %} + + + + + {% endfor %} + + + + + + {% endif %} + + + + + + + + + + +
    {% trans 'Transaction type' %}{% trans 'Price' %}
    {{ entry_name }}€ {{ entry_sum|intcomma }}
    {% trans 'Subtotal' %}€ {{ totals|last|intcomma }}
    {% trans 'Payments' %}€ {{ payment_totals|intcomma }}
    {% trans 'Balance' %}€ {{ sums_balance|intcomma }}
    +
    + +

    {% trans 'Transactions per type' %}

    + {% if not has_transactions %} +

    {% trans 'No unpaid transactions have been found' %}

    + {% else %} +

    {% trans 'Personal tab' %}

    + {% include "lists/cookie_corner_transactions.html" with transactions=all_transactions.0 totals=totals.0 %} + +

    {% trans 'Activities' %}

    + {% include "lists/activity_transactions.html" with transactions=all_transactions.1 totals=totals.1 %} + +

    {% trans 'Alexia' %}

    + {% include "lists/alexia_transactions.html" with transactions=all_transactions.2 totals=totals.2 %} + +

    {% trans 'Contribution' %}

    + {% include "lists/contribution_transactions.html" with transactions=all_transactions.3 totals=totals.3 %} + +

    {% trans 'Other' %}

    + {% include "lists/custom_transactions.html" with transactions=all_transactions.4 totals=totals.4 %} + +

    {% trans 'Payments' %}

    + {% include "lists/payment_transactions.html" with transactions=payment_transactions totals=payment_totals %} + {% endif %} +
    +
    + +{% endblock content %} diff --git a/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html b/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html new file mode 100644 index 00000000..4c6b217c --- /dev/null +++ b/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html @@ -0,0 +1,188 @@ +{% extends "basis.html" %} +{% load unpaid_transaction_helpers %} +{% load humanize %} +{% load i18n %} + +{% block titel %}{% blocktrans with page=page_obj.number total=page_obj.paginator.num_pages %}Unpaid transactions - Page {{ page }}/{{ total }}{% endblocktrans %}{% endblock titel %} + +{% block content %} +
    +

    {% blocktrans with page=page_obj.number total=page_obj.paginator.num_pages %}Unpaid transactions - Page {{ page }}/{{ total }}{% endblocktrans %}

    +
    +

    {% trans 'These are people with transactions that have not been paid for.' %}

    + + {% if filter_no_mandate %}

    + {% if filter_mandate_type %} + {% blocktrans with filtered_type=filter_mandate_type %} + Only people without a mandate of type '{{ filtered_type }}' are shown. + {% endblocktrans %} + {% else %} + {% trans 'Only people without a mandate are shown.' %} + {% endif %} +

    {% endif %} + + {% if filter_only_mandate %}

    + {% if filter_mandate_type %} + {% blocktrans with filtered_type=filter_mandate_type %} + Only people with a mandate of type 'filtered_type' are shown. + {% endblocktrans %} + {% else %} + {% trans 'Only people with a mandate are shown.' %} + {% endif %} +

    {% endif %} + + {% if filter_transactions %}

    + {% blocktrans with filtered_type=filter_transactions %} + Only transactions of type '{{ filter_transactions }}' are shown. + {% endblocktrans %} +

    {% endif %} + +

    {% trans 'Each person can be expanded to view details about the individual transactions.' %}

    + + + + + + + + + + + + + + +
    {% trans 'Total number of people' %}{{ person_count|default_if_none:0 }}
    {% trans 'Total number of transactions' %}{{ transaction_count|default_if_none:0 }}
    {% trans 'Total sum of unpaid costs' %}€ {{ total_costs|default_if_none:0|intcomma }}
    + +

    {% trans 'Filter options' %}

    + + + + + + + + + + + + + +
    {% trans 'Only people with:' %} + {% trans 'Contribution mandate' %} + {% trans 'Consumption mandate' %} + {% trans 'Any mandate' %} +
    {% trans 'Only people without:' %} + {% trans 'Contribution mandate' %} + {% trans 'Consumption mandate' %} + {% trans 'Any mandate' %} +
    {% trans 'Only transactions of type:' %} + {% trans 'Contribution transactions' %} + {% trans 'Activity transactions' %} + {% trans 'Cookie corner transactions' %} + {% trans 'Alexia transactions' %} + {% trans 'Custom transactions' %} + {% trans 'Reversal transactions' %} +
    + {% if filter_no_mandate or filter_only_mandate or filter_transactions %} + {% trans 'Clear filters' %} + {% endif %} + + {% trans 'Toggle expand/collapse all' %} + + {% querystring page=None as qstring %} + {% include 'paginator.html' with page=page_obj query=qstring|slice:'1:' %} +
    +
    + + {% if page_obj.object_list %} + {% for person in page_obj.object_list.select_related %} +
    +

    + {% with unpaid_transactions=person|unpaid_transactions_of_type:filter_transactions unpaid_costs=person|unpaid_transactions_total_cost_of_type:filter_transactions %} + {% blocktrans with name=person.incomplete_name num_transactions=unpaid_transactions.count total_cost=unpaid_costs|intcomma %} + {{ name }} - ({{ num_transactions }}) - € {{ total_cost }} + {% endblocktrans %} + {% endwith %} +

    +
    + +
    +
    + {% endfor %} + {% else %} +
    +

    {% trans 'No unpaid transactions' %}

    +
    +

    {% trans 'There are no people with unpaid transactions! Woah! 🎉🎉🎉' %}

    +
    +
    + {% endif %} + + +{% endblock content %} diff --git a/amelie/personal_tab/templates/debt_collection/instruction_view.html b/amelie/personal_tab/templates/debt_collection/instruction_view.html index 6f9c0342..e3fe51d0 100644 --- a/amelie/personal_tab/templates/debt_collection/instruction_view.html +++ b/amelie/personal_tab/templates/debt_collection/instruction_view.html @@ -24,7 +24,12 @@

    {% trans 'Direct withdrawal-instruction' %}

    {% if instruction.authorization.person %} - {{ instruction.authorization.person }} + {{ instruction.authorization.person }} + +   + + {% else %} — @@ -68,7 +73,7 @@

    {% trans 'Direct withdrawal-instruction' %}

    {% trans 'Price' %} - {{ instruction.amount }} + € {{ instruction.amount }} {% if request.is_board %} diff --git a/amelie/personal_tab/templates/debt_collection/person_instructions.html b/amelie/personal_tab/templates/debt_collection/person_instructions.html index 223b8c36..3f776b3a 100644 --- a/amelie/personal_tab/templates/debt_collection/person_instructions.html +++ b/amelie/personal_tab/templates/debt_collection/person_instructions.html @@ -28,7 +28,7 @@

    {% blocktrans %}Direct debit for {{ person }}{% endblocktrans %}

    {{ instruction.description }} - {{ instruction.amount }} + € {{ instruction.amount }} {% endfor %} diff --git a/amelie/personal_tab/templates/debt_collection/process_batch.html b/amelie/personal_tab/templates/debt_collection/process_batch.html index d550dd30..e44b1966 100644 --- a/amelie/personal_tab/templates/debt_collection/process_batch.html +++ b/amelie/personal_tab/templates/debt_collection/process_batch.html @@ -30,7 +30,7 @@

    {{ batch.reference_number }}

    {% trans 'Total cost' %} - {{ batch.control_sum }} + € {{ batch.control_sum }} {% trans 'Status' %} @@ -65,6 +65,11 @@

    {{ batch.reference_number }}

    {{ instruction.authorization.person }} +   + + + {% else %} — {% endif %} @@ -82,7 +87,7 @@

    {{ batch.reference_number }}

    — {% endif %} - {{ instruction.amount }} + € {{ instruction.amount }} {{ instruction.description }} diff --git a/amelie/personal_tab/templates/debt_collection/view.html b/amelie/personal_tab/templates/debt_collection/view.html index 7e5a0663..e6c91405 100644 --- a/amelie/personal_tab/templates/debt_collection/view.html +++ b/amelie/personal_tab/templates/debt_collection/view.html @@ -27,11 +27,11 @@

    {% trans 'direct withdrawal-task' %}

    {% trans 'Total reversed' %} - {{ assignment.reversed_sum }} + € {{ assignment.reversed_sum }} {% trans 'Total cost' %} - {{ assignment.control_sum }} + € {{ assignment.control_sum }} @@ -70,11 +70,11 @@

    {{ batch.reference_number }}

    {% trans 'Total reversed' %} - {{ batch.reversed_sum }} + € {{ batch.reversed_sum }} {% trans 'Total cost' %} - {{ batch.control_sum }} + € {{ batch.control_sum }} {% trans 'Status' %} @@ -108,6 +108,11 @@

    {{ batch.reference_number }}

    {{ instruction.authorization.person }} +   + + + {% else %} — {% endif %} @@ -125,7 +130,7 @@

    {{ batch.reference_number }}

    — {% endif %} - {{ instruction.amount }} + € {{ instruction.amount }} {{ instruction.description }} diff --git a/amelie/personal_tab/templates/info/debt_collection_instructions.html b/amelie/personal_tab/templates/info/debt_collection_instructions.html deleted file mode 100644 index 96c2f41f..00000000 --- a/amelie/personal_tab/templates/info/debt_collection_instructions.html +++ /dev/null @@ -1,48 +0,0 @@ -{% load i18n %} - -
    -

    {% trans "Direct debits" %}

    - -
    - {% if debt_collection_instructions %} -

    - {% blocktrans with debt_collection_instructions|length as amount %} - Last {{ amount }} executed or planned direct debits. - {% endblocktrans %} -

    -
    - - - - - - - - - - {% for instruction in debt_collection_instructions %} - - - - - - {% endfor %} - -
    {% trans 'Date' %}{% trans 'Description' %}{% trans 'Price' %}
    - {{ instruction.batch.execution_date }} - - - {{ instruction.description }} - - {{ instruction.amount }}
    -
    -

    - - {% trans "View all direct debits." %} - -

    - {% else %} -

    {% trans "There are no direct debits executed or planned yet." %}

    - {% endif %} -
    -
    diff --git a/amelie/personal_tab/templates/info/payments.html b/amelie/personal_tab/templates/info/payments.html new file mode 100644 index 00000000..36ea48b2 --- /dev/null +++ b/amelie/personal_tab/templates/info/payments.html @@ -0,0 +1,117 @@ +{% load i18n %} + +
    +

    {% trans "Payments" %}

    + +
    +
    +
    +

    {% trans "Direct debits" %}

    +

    + {% blocktrans count counter=debt_collection_instructions|length %} + Your most recently executed or planned direct debit. + {% plural %} + Your most recently executed or planned direct debits. + {% endblocktrans %} +

    +
    + + + + + + + + + + {% for instruction in debt_collection_instructions %} + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans 'Date' %}{% trans 'Description' %}{% trans 'Price' %}
    + {{ instruction.batch.execution_date }} + + + {{ instruction.description }} + + € {{ instruction.amount }}
    +

    {% trans "There are no direct debits executed or planned yet." %}

    +
    +
    + {% if debt_collection_instructions %} +

    + + {% trans "View all direct debits." %} + +

    + {% endif %} +
    +
    +

    + {% trans "Manual payments" %} + {% if request.is_board %} + + {% trans 'Add manual payment' %} + + {% endif %} +

    +

    + {% blocktrans count counter=manual_payment_settlements|length %} + Your most recent manual payment. + {% plural %} + Your most recent manual payments. + {% endblocktrans %} +

    +
    + + + + + + + + + + + {% for settlement in manual_payment_settlements %} + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans 'Date' %}{% trans 'Description' %}{% trans 'Payment method' %}{% trans 'Price' %}
    + {{ settlement.payment_date }} + + + {{ settlement.description }} + + {{ settlement.payment_method.name }}€ {{ settlement.amount }}
    +

    {% trans "There are no manual payments yet." %}

    +
    +
    + {% if manual_payment_settlements %} +

    + + {% trans "View all manual payments." %} + +

    + {% endif %} +
    +
    +
    +
    diff --git a/amelie/personal_tab/templates/info/transaction_information.html b/amelie/personal_tab/templates/info/transaction_information.html index 0923d625..3edb6e2a 100644 --- a/amelie/personal_tab/templates/info/transaction_information.html +++ b/amelie/personal_tab/templates/info/transaction_information.html @@ -1,4 +1,8 @@ {% load i18n %} + + {% trans 'Transaction type' %} + {{ object.transaction_type.1 }} + {% trans 'Date' %} {{ object.date }} @@ -9,19 +13,12 @@ {% trans 'Price' %} - {{ object.price }} + € {{ object.price }} - {% trans 'Direct withdrawal' %} + {% trans 'Settled in' %} - {% if object.debt_collection %} - - {{ object.debt_collection }} -
    - {{ object.debt_collection.batch.execution_date }} - {% else %} - — - {% endif %} + {% include 'info/transaction_settlement_info.html' with settlement=object.settlement %} {% if object.discount %} @@ -39,7 +36,14 @@ {% trans 'Person' %} {% if object.person %} - {{ object.person }} + + {{ object.person }} + +   + + + {% else %} — {% endif %} @@ -53,4 +57,76 @@ {% trans 'Added by' %} {{ object.added_by|default:"—" }} + {% if object.transaction_type.0 == "activitytransaction" %} + + {% trans 'Activities' %} + + {% if object.activitytransaction.event %} + {% if request.is_board %} + {{ object.activitytransaction.event.summary }} + {% else %} + {{ object.activitytransaction.event.summary }} + {% endif %} + {% else %} + {% trans 'Deleted event' %} + {% endif %} + + + {% if object.activitytransaction.participation.remark %} + + {% trans 'Remarks' %} + {{ object.activitytransaction.participation.remark }} + + {% endif %} + {% elif object.transaction_type.0 == "alexiatransaction" %} + + {% trans 'Transaction number Alexia' %} + {{ object.alexiatransaction.transaction_id }} + + {% elif object.transaction_type.0 == "cookiecornertransaction" %} + + {% trans 'Article' %} + {{ object.cookiecornertransaction.article|default:"—" }} + + + {% trans 'Amount' %} + {{ object.cookiecornertransaction.amount }} + + + {% trans 'kCal' %} + {{ object.cookiecornertransaction.kcal|default_if_none:"—" }} + + {% elif object.transaction_type.0 == "contributiontransaction" %} + + {% trans 'Membership' %} + {% with membership=object.contributiontransaction.membership %} + {{ membership.type.name }} ({{ membership.year }}/{{ membership.year|add:'1' }}) + {% endwith %} + + {% elif object.transaction_type.0 == "reversaltransaction" %} + + {% trans 'Reversed collection' %} + + + {{ object.reversaltransaction.reversal.instruction }} +
    + {{ object.reversaltransaction.reversal.instruction.batch.execution_date }} + + + + {% trans 'Reason' %} + {{ object.reversaltransaction.reversal.get_reason_display }} + + {% elif object.transaction_type.0 == "settlementextrabalancetransaction" or object.transaction_type.0 == "settlementmanualpaymenttransaction" %} + + {% trans 'Originated in' %} + + {% if object.transaction_type.0 == "settlementextrabalancetransaction" %} + {% include 'info/transaction_settlement_info.html' with settlement=object.settlementextrabalancetransaction.manual_settlement %} + {% else %} + {% include 'info/transaction_settlement_info.html' with settlement=object.settlementmanualpaymenttransaction.manual_settlement %} + {% endif %} + + + {% endif %} {% endif %} diff --git a/amelie/personal_tab/templates/info/transaction_settlement_info.html b/amelie/personal_tab/templates/info/transaction_settlement_info.html new file mode 100644 index 00000000..19e396dd --- /dev/null +++ b/amelie/personal_tab/templates/info/transaction_settlement_info.html @@ -0,0 +1,44 @@ +{% load i18n %} +{% if settlement %} + + {{ settlement }} +
    + {% if settlement.settlement_type == "INSTR" %} + + + {% blocktrans with date=settlement.debtcollectioninstruction.batch.execution_date %} + Direct debit on {{ date }} + {% endblocktrans %} + + {% if settlement.debtcollectioninstruction.reversal %} +
    + + + {% blocktrans with date=settlement.debtcollectioninstruction.reversal.date %} + Reversal on {{ date }} + {% endblocktrans %} + + {% endif %} + {% elif settlement.settlement_type == "MANUAL" %} + + {% blocktrans with date=settlement.manualpaymentsettlement.payment_date %} + Manual payment on {{ date }} + {% endblocktrans %} + +
    + + + {{ settlement.manualpaymentsettlement.payment_method.name }} + + {% else %} + + {% trans 'UNKNOWN payment!' %} + + {% endif %} +{% else %} + {% if highlight_unpaid %} + {% trans 'Unpaid!' %} + {% else %} + — + {% endif %} +{% endif %} diff --git a/amelie/personal_tab/templates/lists/activity_transactions.html b/amelie/personal_tab/templates/lists/activity_transactions.html index f6940183..68cf46da 100644 --- a/amelie/personal_tab/templates/lists/activity_transactions.html +++ b/amelie/personal_tab/templates/lists/activity_transactions.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if transactions %} @@ -22,7 +23,14 @@ {% if transaction.person %} {% if request.is_board %} - {{ transaction.person }} + + {{ transaction.person }} + +   + + + {% else %} {{ transaction.person }} {% endif %} @@ -47,14 +55,14 @@ {{ transaction.with_enrollment_options|yesno:"Ja,Nee" }} - {{ transaction.price }} + € {{ transaction.price|intcomma }} {% endfor %} {% if totals %} - + {% trans 'Total' %} - {{ totals }} + € {{ totals|intcomma }} {% endif %} diff --git a/amelie/personal_tab/templates/lists/alexia_transactions.html b/amelie/personal_tab/templates/lists/alexia_transactions.html index 5fe71279..2a047c10 100644 --- a/amelie/personal_tab/templates/lists/alexia_transactions.html +++ b/amelie/personal_tab/templates/lists/alexia_transactions.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if transactions %} @@ -20,7 +21,14 @@ {% if transaction.person %} {% if request.is_board %} - {{ transaction.person }} + + {{ transaction.person }} + +   + + + {% else %} {{ transaction.person }} {% endif %} @@ -33,14 +41,14 @@ {{ transaction.description }} - {{ transaction.price }} + € {{ transaction.price|intcomma }} {% endfor %} {% if total %} - + {% trans 'Total' %} - {{ totals }} + € {{ totals|intcomma }} {% endif %} diff --git a/amelie/personal_tab/templates/lists/authorization_select.html b/amelie/personal_tab/templates/lists/authorization_select.html index 3be88208..eb13c73a 100644 --- a/amelie/personal_tab/templates/lists/authorization_select.html +++ b/amelie/personal_tab/templates/lists/authorization_select.html @@ -35,6 +35,11 @@ {{ authorization.person }} +   + + + {% else %} — {% endif %} diff --git a/amelie/personal_tab/templates/lists/contribution_transactions.html b/amelie/personal_tab/templates/lists/contribution_transactions.html new file mode 100644 index 00000000..e879cd8d --- /dev/null +++ b/amelie/personal_tab/templates/lists/contribution_transactions.html @@ -0,0 +1,59 @@ +{% load humanize %} +{% load i18n %} + +{% if transactions %} +
    + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + {% endfor %} + {% if total %} + + + + + + {% endif %} + +
    {% trans 'Date' %}{% trans 'Time' %}{% trans 'Person' %}{% trans 'Description' %}{% trans 'Price' %}
    {{ transaction.date|date:"d F Y" }}{{ transaction.date|date:"G:i"}} + {% if transaction.person %} + {% if request.is_board %} + + {{ transaction.person }} + +   + + + + {% else %} + {{ transaction.person }} + {% endif %} + {% else %} + — + {% endif %} + + + {{ transaction.description }} + + € {{ transaction.price|intcomma }}
    {% trans 'Total' %}€ {{ totals|intcomma }}
    +
    +{% else %} +

    {% trans 'No contribution transactions.' %}

    +{% endif %} diff --git a/amelie/personal_tab/templates/lists/cookie_corner_transactions.html b/amelie/personal_tab/templates/lists/cookie_corner_transactions.html index 983a6860..e9ef9f46 100644 --- a/amelie/personal_tab/templates/lists/cookie_corner_transactions.html +++ b/amelie/personal_tab/templates/lists/cookie_corner_transactions.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if transactions %} @@ -23,7 +24,14 @@ {% if transaction.person %} {% if request.is_board %} - {{ transaction.person }} + + {{ transaction.person }} + +   + + + {% else %} {{ transaction.person }} {% endif %} @@ -38,15 +46,15 @@ {{ transaction.amount }} {{ transaction.discount.amount|default:"—" }} - {{ transaction.price }} + € {{ transaction.price|intcomma }} {{ transaction.kcal|default:"—" }} {% endfor %} {% if totals %} - + {% trans 'Total' %} - {{ totals }} + € {{ totals|intcomma }} {{ kcal_totals }} {% endif %} diff --git a/amelie/personal_tab/templates/lists/custom_transactions.html b/amelie/personal_tab/templates/lists/custom_transactions.html index a4978592..8a78dadc 100644 --- a/amelie/personal_tab/templates/lists/custom_transactions.html +++ b/amelie/personal_tab/templates/lists/custom_transactions.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if transactions %} @@ -20,7 +21,14 @@ {% if transaction.person %} {% if request.is_board %} - {{ transaction.person }} + + {{ transaction.person }} + +   + + + {% else %} {{ transaction.person }} {% endif %} @@ -33,14 +41,14 @@ {{ transaction.description }} - {{ transaction.price }} + € {{ transaction.price|intcomma }} {% endfor %} {% if total %} - + {% trans 'Total' %} - {{ totals }} + € {{ totals|intcomma }} {% endif %} diff --git a/amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html b/amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html index 9c9eaa06..cd074de4 100644 --- a/amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html +++ b/amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if rows %} @@ -9,7 +10,7 @@ {% trans 'Person' %} {% trans 'Membership type' %} {% trans 'Mandate' %} - {% trans 'Price' %} + {% trans 'Price' %} @@ -18,22 +19,31 @@ {% if enabled %} + name="contribution_{{ row.transaction.membership.id }}" value="true"/> {% endif %} {{ row.person }} +   + + + - {{ row.membership.type }} + {{ row.transaction.membership.type }} - {{ row.instruction.authorization }} + {% if row.instruction.authorization %} + {{ row.instruction.authorization }} + {% else %} + — + {% endif %} {% if row.instruction.amendment %} ({% trans 'A ' context 'amendment abbr' %}) {% endif %} - {{ row.sumf }} + € {{ row.sum|intcomma }} @@ -43,14 +53,14 @@ {% endfor %} - + {% if enabled %} {% endif %} {% trans 'Total' %} - {{ total }} + € {{ total|intcomma }} diff --git a/amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html b/amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html index 324da830..a4723d0f 100644 --- a/amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html +++ b/amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if rows %} @@ -8,7 +9,7 @@ {% if enabled %}{% trans 'Collect' %}{% endif %} {% trans 'Person' %} {% trans 'Mandate' %} - {% trans 'Price' %} + {% trans 'Price' %} @@ -24,30 +25,41 @@ {{ row.person }} +   + + + - {{ row.authorization }} + {% if row.authorization %} + {{ row.authorization }} + {% else %} + — + {% endif %} {% if row.instruction.amendment %} ({% trans 'A ' context 'amendment abbr' %}) {% endif %} - {{ row.sumf }} - - - - {{ row.instruction.description }} - + € {{ row.sum|intcomma }} + {% if row.instruction.description %} + + + {{ row.instruction.description }} + + + {% endif %} {% endfor %} - + {% if enabled %} {% endif %} {% trans 'Total' %} - {{ total }} + € {{ total|intcomma }} diff --git a/amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html b/amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html index 42c9d9b2..e9eea398 100644 --- a/amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html +++ b/amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n transaction_link %} {% if rows %} @@ -41,15 +42,15 @@ {% for amount in row.amounts %} - {{ amount }} + € {{ amount|intcomma }} {% endfor %} {% endfor %} - + {% trans 'Total' %} {% for total in totals %} - {{ total }} + € {{ total|intcomma }} {% endfor %} diff --git a/amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html b/amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html index b0f0977b..50951196 100644 --- a/amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html +++ b/amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if transactions %} @@ -20,7 +21,14 @@ {% if transaction.person %} {% if request.is_board %} - {{ transaction.person }} + + {{ transaction.person }} + +   + + + {% else %} {{ transaction.person }} {% endif %} @@ -29,14 +37,14 @@ {% endif %} {{ transaction.description }} - {{ transaction.price }} + € {{ transaction.price|intcomma }} {% endfor %} {% if total %} - + {% trans 'Total' %} - {{ totals }} + € {{ totals|intcomma }} {% endif %} diff --git a/amelie/personal_tab/templates/lists/payment_transactions.html b/amelie/personal_tab/templates/lists/payment_transactions.html new file mode 100644 index 00000000..e6c30d9e --- /dev/null +++ b/amelie/personal_tab/templates/lists/payment_transactions.html @@ -0,0 +1,79 @@ +{% load class_helpers %} +{% load humanize %} +{% load i18n %} + +{% if transactions.0 or transactions.1 or transactions.2 or transactions.3 %} +
    + + + + + + + + + + + + + {% for transaction_set in transactions %} + {% for transaction in transaction_set %} + + + + + + + + + {% endfor %} + {% endfor %} + {% if total %} + + + + + + {% endif %} + +
    {% trans 'Date' %}{% trans 'Time' %}{% trans 'Person' %}{% trans 'Payment method' %}{% trans 'Description' %}{% trans 'Price' %}
    {{ transaction.date|date:"d F Y" }}{{ transaction.date|date:"G:i"}} + {% if transaction.person %} + {% if request.is_board %} + + {{ transaction.person }} + +   + + + + {% else %} + {{ transaction.person }} + {% endif %} + {% else %} + — + {% endif %} + + {% if transaction|get_class == "DebtCollectionTransaction" %} + {% trans 'Direct debit' %} + {% elif transaction|get_class == "ReversalTransaction" %} + {% trans 'Reversal of direct debit' %} + {% elif transaction|get_class == "ExtraManualPaymentTransaction" %} + {{ transaction.settlement.manualpaymentsettlement.payment_method.name|default:_("Unknown") }} + {% elif transaction|get_class == "ManualPaymentSettlementTransaction" %} + {{ transaction.settlement.manualpaymentsettlement.payment_method.name|default:_("Unknown") }} + {% else %} + {% trans 'Unknown' %} + {% endif %} + + + {{ transaction.description }} + {% if transaction.settlement.description %}
    + + {{ transaction.settlement.description }} + {% endif %} +
    € {{ transaction.price|intcomma }}
    {% trans 'Total' %}€ {{ totals|intcomma }}
    +
    +{% else %} +

    {% trans 'No payments.' %}

    +{% endif %} diff --git a/amelie/personal_tab/templates/lists/transaction_row.html b/amelie/personal_tab/templates/lists/transaction_row.html new file mode 100644 index 00000000..921ebb05 --- /dev/null +++ b/amelie/personal_tab/templates/lists/transaction_row.html @@ -0,0 +1,33 @@ +{% load humanize %} +{% load i18n %} + + {{ transaction.date|date:"d F Y" }} + {{ transaction.date|date:"G:i"}} + + {% if transaction.person %} + {% if request.is_board %} + + {{ transaction.person }} + + {% url 'personal_tab:dashboard' transaction.person.pk transaction.person.slug as personal_overview %} + {% if request.path != personal_overview %} +   + + + + {% endif %} + {% else %} + {{ transaction.person }} + {% endif %} + {% else %} + — + {% endif %} + + + + {{ transaction.description }} + + + € {{ transaction.price|intcomma }} + {% if not hide_kcal %}{{ transaction.cookiecornertransaction.kcal|default:"—" }}{% endif %} + diff --git a/amelie/personal_tab/templates/lists/transactions.html b/amelie/personal_tab/templates/lists/transactions.html index fa2b26a7..79fe5144 100644 --- a/amelie/personal_tab/templates/lists/transactions.html +++ b/amelie/personal_tab/templates/lists/transactions.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% if transactions %} @@ -10,40 +11,19 @@ {% trans 'Person' %} {% trans 'Description' %} {% trans 'Price' %} - {% trans 'kCal' %} + {% if not hide_kcal %}{% trans 'kCal' %}{% endif %} {% for transaction in transactions %} - - {{ transaction.date|date:"d F Y" }} - {{ transaction.date|date:"G:i"}} - - {% if transaction.person %} - {% if request.is_board %} - {{ transaction.person }} - {% else %} - {{ transaction.person }} - {% endif %} - {% else %} - — - {% endif %} - - - - {{ transaction.description }} - - - {{ transaction.price }} - {{ transaction.cookiecornertransaction.kcal|default:"—" }} - + {% include 'lists/transaction_row.html' with transaction=transaction hide_kcal=hide_kcal %} {% endfor %} {% if total %} - + {% trans 'Total' %} - {{ totals }} - {{ kcal_totals }} + € {{ totals|intcomma }} + {% if not hide_kcal %}{{ kcal_totals }}{% endif %} {% endif %} diff --git a/amelie/personal_tab/templates/lists/transactions_categorized.html b/amelie/personal_tab/templates/lists/transactions_categorized.html new file mode 100644 index 00000000..ada26f16 --- /dev/null +++ b/amelie/personal_tab/templates/lists/transactions_categorized.html @@ -0,0 +1,49 @@ +{% load humanize %} +{% load i18n %} + +{% if transactions %} +
    + + + + + + + + + {% if not hide_kcal %}{% endif %} + + + + {% for category in transactions %} + {% if category.title %} + + + + {% endif %} + {% for transaction in category.transactions %} + {% include 'lists/transaction_row.html' with transaction=transaction hide_kcal=hide_kcal %} + {% endfor %} + {% if category.show_total %} + + + + + {% if not hide_kcal %}{% endif %} + + {% endif %} + {% endfor %} + {% if total %} + + + + + {% if not hide_kcal %}{% endif %} + + {% endif %} + +
    {% trans 'Date' %}{% trans 'Time' %}{% trans 'Person' %}{% trans 'Description' %}{% trans 'Price' %}{% trans 'kCal' %}
    {{ category.title }}
    {% trans 'Subtotal' %}€ {{ category.totals|intcomma }}{{ category.kcal_totals }}
    {% trans 'Total' %}€ {{ totals|intcomma }}{{ kcal_totals }}
    +
    +{% else %} +

    {% trans 'No transactions' %}

    +{% endif %} diff --git a/amelie/personal_tab/templates/lists/transactions_totals.html b/amelie/personal_tab/templates/lists/transactions_totals.html index 615140cb..d752462e 100644 --- a/amelie/personal_tab/templates/lists/transactions_totals.html +++ b/amelie/personal_tab/templates/lists/transactions_totals.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n transaction_link %} {% if rows %} @@ -21,9 +22,12 @@ {% trans 'Personal tab' %} {% trans 'Activities' %} {% trans 'Alexia' %} + {% trans 'Contribution' %} {% trans 'Other' %} {% trans 'Total' %} + {% trans 'Payments' %} + {% trans 'kCal' %} @@ -42,22 +46,30 @@ {% if overview_type == 'total' %} {{ row.date.year }} {% endif %} + + {% if row.date > date_now %} +   + {% endif %} {% for amount in row.amounts %} - {{ amount }} + € {{ amount|intcomma }} {% endfor %} + € {{ row.payment_totals|intcomma }} + {{ row.kcal_total }} {% endfor %} - + {% trans 'Total' %} {% for total in totals %} - {{ total }} + € {{ total|intcomma }} {% endfor %} + € {{ payment_totals|intcomma }} + {{ kcal_totals }} diff --git a/amelie/personal_tab/templates/personal_tab/balance_reminder.mail b/amelie/personal_tab/templates/personal_tab/balance_reminder.mail new file mode 100644 index 00000000..f3b66849 --- /dev/null +++ b/amelie/personal_tab/templates/personal_tab/balance_reminder.mail @@ -0,0 +1,21 @@ +{% extends "iamailer/email_basic.mail" %} +{% load i18n htmlify only %} + +{% block subject %}{% blocktrans %}[Inter-Actief] Reminder of open balance on your personal tab{% endblocktrans %}{% endblock %} + +{% block content %}{% htmlify %}{% blocktrans %}Dear{% endblocktrans %} {{ recipient.first_name }}, + +{% blocktrans %}This e-mail serves as a reminder that you have an open balance on your personal tab that needs to be paid.{% endblocktrans %} + +{% blocktrans %}Because you do not have an active, signed mandate for consumptions, we cannot collect this money from your bank automatically, so you will need to pay manually. Please visit the association room or contact the board to pay the balance by cash, bank transfer or PIN.{% endblocktrans %} + +{% blocktrans with costs=balance num_unpaid=num_unpaid_transactions %}Your open balance is {{ costs }} euro. This consists of {{ num_unpaid }} transactions.{% endblocktrans %} + +{% onlyhtml %}{% blocktrans with link=overview_link %}To view more details about the balance and the unpaid transactions, you can view your unpaid transaction overview.{% endblocktrans %} +{% endonlyhtml %}{% onlyplain %}{% blocktrans with link=overview_link %}To view more details about the balance and the unpaid transactions, you can view your unpaid transaction overview at {{ link }}{% endblocktrans %} +{% endonlyplain %} +{% blocktrans %}If you have any questions you can contact us by replying to this email.{% endblocktrans %} + +{% blocktrans %}Kind regards, + +The treasurer of Inter-Actief{% endblocktrans %}{% endhtmlify %}{% endblock %} diff --git a/amelie/personal_tab/templates/print/log_list.html b/amelie/personal_tab/templates/print/log_list.html index 004e96ed..f0af6175 100644 --- a/amelie/personal_tab/templates/print/log_list.html +++ b/amelie/personal_tab/templates/print/log_list.html @@ -52,6 +52,11 @@

    {% trans "Print Log" %}

    {{ entry.actor }} +   + + + {% if entry.job_id %} diff --git a/amelie/personal_tab/templates/statistics/statistics.html b/amelie/personal_tab/templates/statistics/statistics.html index 028420ec..d09bc60e 100644 --- a/amelie/personal_tab/templates/statistics/statistics.html +++ b/amelie/personal_tab/templates/statistics/statistics.html @@ -27,13 +27,13 @@

    {% for row in tables.t.rows %} {{ row.0 }} - {{ row.1|intcomma }} + € {{ row.1|intcomma }} {% endfor %} {% trans 'Total costs' %} - {{ tables.t.sum|intcomma }} + € {{ tables.t.sum|intcomma }} {% endif %} @@ -62,14 +62,14 @@

    {% trans 'Personal tab statistics' %}

    {% for category in tables.u.rows %} - + {{ category.category }} {{ category.count }} - {{ category.discount|stringformat:".2f" }} - {{ category.sum|stringformat:".2f" }} - {{ category.total|stringformat:".2f" }} + € {{ category.discount|intcomma }} + € {{ category.sum|intcomma }} + € {{ category.total|intcomma }} {% for article in category.articles %} @@ -77,9 +77,9 @@

    {% trans 'Personal tab statistics' %}

    {{ article.article }} {{ article.article.ledger_account|default:"—" }} {{ article.count }} - {{ article.discount|stringformat:".2f" }} - {{ article.sum|stringformat:".2f" }} - {{ article.total|stringformat:".2f" }} + {% if article.discount %}€ {{ article.discount|intcomma }}{% endif %} + € {{ article.sum|intcomma }} + € {{ article.total|intcomma }} {% endfor %} {% endfor %} @@ -108,20 +108,20 @@

    {% trans 'Personal tab balance' %}

    {% for row in tables.s.rows %} - + {{ row.0|default:"—" }} {{ row.1 }} - {{ row.2|stringformat:".2f" }} - {{ row.3|stringformat:".2f" }} - {{ row.4|stringformat:".2f" }} + € {{ row.2|intcomma }} + € {{ row.3|intcomma }} + € {{ row.4|intcomma }} {% endfor %} - + {% trans 'Total' %} {{ tables.s.totals.0 }} - {{ tables.s.totals.1 }} - {{ tables.s.totals.2 }} - {{ tables.s.totals.3 }} + € {{ tables.s.totals.1|intcomma }} + € {{ tables.s.totals.2|intcomma }} + € {{ tables.s.totals.3|intcomma }} @@ -142,7 +142,7 @@

    {{ table.title }}

    {% trans 'Article' %} {% trans 'Amount' %} - {% trans 'discount' %} + {% trans 'Discount' %} {% trans 'Total cost' %} {% trans 'Total costs' %} @@ -152,17 +152,17 @@

    {{ table.title }}

    {{ article.article }} {{ article.count }} - {{ article.discount|stringformat:".2f" }} - {{ article.sum|stringformat:".2f" }} - {{ article.total|stringformat:".2f" }} + {% if article.discount %}€ {{ article.discount|intcomma }}{% endif %} + € {{ article.sum|intcomma }} + € {{ article.total|intcomma }} {% endfor %} - + {% trans 'Total' %} {{ table.total.count }} - {{ table.total.discount|stringformat:".2f" }} - {{ table.total.sum|stringformat:".2f" }} - {{ table.total.total|stringformat:".2f" }} + € {{ table.total.discount|intcomma }} + € {{ table.total.sum|intcomma }} + € {{ table.total.total|intcomma }} @@ -191,7 +191,7 @@

    {% trans 'Activities' %}

    {% for row in tables.a.rows %} - + {{ row.event.begin|date:"d F Y" }} {{ row.event.begin|date:"G:i"}} @@ -207,16 +207,16 @@

    {% trans 'Activities' %}

    {{ row.count }} {{ row.active_count }} - {{ row.sum }} + € {{ row.sum|intcomma }} {% endfor %} - + {% trans 'Total' %} {{ tables.a.count }} {{ tables.a.active_count }} - {{ tables.a.sum }} + € {{ tables.a.sum|intcomma }} @@ -254,17 +254,17 @@

    {% trans 'Alexia transactions' %}

    {{ row.description }} - {{ row.price__sum }} + € {{ row.price__sum|intcomma }} {% endfor %} - + {% trans 'Total' %} - {{ tables.x.sum }} + € {{ tables.x.sum|intcomma }} @@ -289,16 +289,16 @@

    {% trans 'Membership fee' %}

    {% for row in tables.c.rows %} - + {{ row.membership_type }} {{ row.amount }} - {{ row.total }} + € {{ row.total|intcomma }} {% endfor %} - + {% trans 'Total' %} - {{ tables.c.sum }} + € {{ tables.c.sum|intcomma }} @@ -344,18 +344,16 @@

    {% trans 'Other' %}

    {{ row.description }} - {{ row.price|stringformat:".2f" }} + € {{ row.price|intcomma }} {% endfor %} - {% if total %} - - - - - {% trans 'Total' %} ({{ tables.o.count }}) - {{ tables.o.sum|stringformat:".2f" }} - - {% endif %} + + + + + {% trans 'Total' %} ({{ tables.o.count }}) + € {{ tables.o.sum|intcomma }} + @@ -382,13 +380,13 @@

    {% trans 'Discount balances' %}

    {{ row.discount_period }} {{ row.count }} - {{ row.sum|stringformat:".2f" }} + € {{ row.sum|intcomma }} {% endfor %} - + {% trans 'Total' %} {{ tables.g.count }} - {{ tables.g.sum|stringformat:".2f" }} + € {{ tables.g.sum|intcomma }} @@ -426,17 +424,17 @@

    {% trans 'Discount offers' %}

    {{ row.count }} - {{ row.sum }} + € {{ row.sum|intcomma }} {% endfor %} - + {% trans 'Total' %} - {{ tables.k.sum }} + € {{ tables.k.sum|intcomma }} diff --git a/amelie/personal_tab/templates/transactions/activity_transaction_detail.html b/amelie/personal_tab/templates/transactions/activity_transaction_detail.html index adb5089e..94db3b7f 100644 --- a/amelie/personal_tab/templates/transactions/activity_transaction_detail.html +++ b/amelie/personal_tab/templates/transactions/activity_transaction_detail.html @@ -10,26 +10,6 @@

    {% trans 'Transaction information' %}

    {% include "info/transaction_information.html" %} - - - - - {% if object.participation.remark %} - - - - - {% endif %}
    {% trans 'Activities' %} - {% if object.event %} - {% if request.is_board %} - {{ object.event.summary }} - {% else %} - {{ object.event.summary }} - {% endif %} - {% else %} - {% trans 'Deleted event' %} - {% endif %} -
    {% trans 'Remarks' %}{{ object.participation.remark }}
    @@ -44,7 +24,7 @@

    {% trans 'Enrollment options' %}

    {% trans 'Question' %} - {% trans 'Respons' %} + {% trans 'Response' %} {% trans 'Costs' %} diff --git a/amelie/personal_tab/templates/transactions/alexia_transaction_detail.html b/amelie/personal_tab/templates/transactions/alexia_transaction_detail.html index d63e0455..676af42b 100644 --- a/amelie/personal_tab/templates/transactions/alexia_transaction_detail.html +++ b/amelie/personal_tab/templates/transactions/alexia_transaction_detail.html @@ -10,10 +10,6 @@

    {% trans 'Transaction information' %}

    {% include "info/transaction_information.html" %} - - - -
    {% trans 'Transaction number Alexia' %}{{ object.transaction_id }}
    diff --git a/amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html b/amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html index 8508e7ac..e1256e62 100644 --- a/amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html +++ b/amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html @@ -10,18 +10,6 @@

    {% trans 'Transaction information' %}

    {% include "info/transaction_information.html" %} - - - - - - - - - - - -
    {% trans 'Article' %}{{ object.article|default:"—" }}
    {% trans 'Amount' %}{{ object.amount }}
    {% trans 'kCal' %}{{ object.kcal|default_if_none:"—" }}
    {% if object.editable and request.is_board %} diff --git a/amelie/personal_tab/templates/transactions/reversal_transaction_detail.html b/amelie/personal_tab/templates/transactions/reversal_transaction_detail.html index badeb1b4..4d8a8322 100644 --- a/amelie/personal_tab/templates/transactions/reversal_transaction_detail.html +++ b/amelie/personal_tab/templates/transactions/reversal_transaction_detail.html @@ -10,19 +10,6 @@

    {% trans 'Transaction information' %}

    {% include "info/transaction_information.html" %} - - - - - - - -
    {% trans 'Reversed collection' %} - - {{ object.reversal.instruction }} -
    - {{ object.reversal.instruction.batch.execution_date }} -
    {% trans 'Reason' %}{{ object.reversal.get_reason_display }}
    diff --git a/amelie/personal_tab/templates/unpaid_memberships/forgive.html b/amelie/personal_tab/templates/unpaid_memberships/forgive.html index 0b8af58f..c0a8236d 100644 --- a/amelie/personal_tab/templates/unpaid_memberships/forgive.html +++ b/amelie/personal_tab/templates/unpaid_memberships/forgive.html @@ -18,12 +18,17 @@

    {% trans 'You are about to forgive the membership fee of the {% trans 'Price' %} - + {% for membership in memberships %} - - {{ membership.member.incomplete_name }} + + {{ membership.member }} + +   + + @@ -51,4 +56,4 @@

    {% trans 'You are about to forgive the membership fee of the -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/amelie/personal_tab/templates/unpaid_memberships/overview.html b/amelie/personal_tab/templates/unpaid_memberships/overview.html index f3107019..69b844a9 100644 --- a/amelie/personal_tab/templates/unpaid_memberships/overview.html +++ b/amelie/personal_tab/templates/unpaid_memberships/overview.html @@ -21,7 +21,7 @@

    {% trans 'Unpaid Memberships' %}

    - {{ year }} + {{ year }}/{{ year|add:1 }} {% for type, count in tuple.0.items %} @@ -37,6 +37,6 @@

    {% trans 'Unpaid Memberships' %}

    - + {% endblock content %} diff --git a/amelie/personal_tab/templates/unpaid_memberships/year_overview.html b/amelie/personal_tab/templates/unpaid_memberships/year_overview.html index 2b858339..84a6e144 100644 --- a/amelie/personal_tab/templates/unpaid_memberships/year_overview.html +++ b/amelie/personal_tab/templates/unpaid_memberships/year_overview.html @@ -4,7 +4,7 @@ {% block titel %}{% trans 'Unpaid Memberships' %} {{ year }}{% endblock titel %} {% block content %} -
    +

    {% trans 'Unpaid Memberships' %} {{ year }}

    @@ -26,15 +26,20 @@

    {{ membership_type.0 }}

    {% trans 'Price' %} - + {% for membership in membership_list %} - - {{ membership.member.incomplete_name }} + + {{ membership.member }} + +   + + @@ -73,7 +78,16 @@

    {{ membership_type.0 }}

    {% trans 'Forgive membership fee' %}

    -
    + + + + {% empty %} +
    +
    +

    {% trans 'No unpaid memberships' %}

    +
    +

    {% trans 'There are no unpaid memberships in this year! Woah! 🎉🎉🎉' %}

    +
    {% endfor %} @@ -98,4 +112,3 @@

    {{ membership_type.0 }}

    } {% endblock content %} - \ No newline at end of file diff --git a/amelie/personal_tab/templatetags/unpaid_transaction_helpers.py b/amelie/personal_tab/templatetags/unpaid_transaction_helpers.py new file mode 100644 index 00000000..3730397a --- /dev/null +++ b/amelie/personal_tab/templatetags/unpaid_transaction_helpers.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + + +@register.filter() +def unpaid_transactions_of_type(person, transaction_type=None): + return person.unpaid_transactions(transaction_type=transaction_type) + + +@register.filter() +def unpaid_transactions_total_cost_of_type(person, transaction_type=None): + return person.unpaid_transactions_total_cost(transaction_type=transaction_type) diff --git a/amelie/personal_tab/transactions.py b/amelie/personal_tab/transactions.py index 34b7ceb6..3a9fe08e 100644 --- a/amelie/personal_tab/transactions.py +++ b/amelie/personal_tab/transactions.py @@ -1,15 +1,17 @@ import datetime import random +from decimal import Decimal +from typing import Optional, Tuple, Union, List, Dict, Any from django.conf import settings from django.db.models import Sum, Q from django.utils import timezone, translation from django.utils.translation import gettext as _ -from amelie.members.models import Study, Student +from amelie.members.models import Study, Student, Person from amelie.personal_tab.models import ActivityTransaction, DiscountPeriod, DiscountCredit, CookieCornerTransaction, \ - Discount -from amelie.tools.logic import current_academic_year_strict + Discount, Transaction, PersonalTabSettlement, ReversalTransaction, Authorization +from amelie.tools.logic import current_academic_year_strict, current_association_year def participation_transaction(participation, reason, cancel=False, added_by=None): @@ -50,26 +52,32 @@ def participation_transaction(participation, reason, cancel=False, added_by=None transaction.save() -def add_participation(participation, added_by=None, is_edited_participation=False): +def add_participation(participation, added_by=None, is_edited_participation=False, reason=None): """Adds a transaction for a participation in an event.""" - with translation.override(participation.person.preferred_language): - if is_edited_participation: - reason = _("Edited enrollment for {activity} (addition of updated costs)").format(activity=participation.event.summary) - else: - reason = _("Enrolled for {activity}").format(activity=participation.event.summary) + if reason is None: + with translation.override(participation.person.preferred_language): + if is_edited_participation: + reason = _("Edited enrollment for {activity} (addition of updated costs)").format( + activity=participation.event.summary + ) + else: + reason = _("Enrolled for {activity}").format(activity=participation.event.summary) participation_transaction(participation, reason, added_by=added_by) -def remove_participation(participation, added_by=None, is_edited_participation=False): +def remove_participation(participation, added_by=None, is_edited_participation=False, reason=None): """Adds a transaction to nullify a participation in an event.""" - with translation.override(participation.person.preferred_language): - if is_edited_participation: - reason = _("Edited enrollment for {activity} (reversal of old costs)").format(activity=participation.event.summary) - else: - reason = _("Unenrolled for {activity}").format(activity=participation.event.summary) + if reason is None: + with translation.override(participation.person.preferred_language): + if is_edited_participation: + reason = _("Edited enrollment for {activity} (reversal of old costs)").format( + activity=participation.event.summary + ) + else: + reason = _("Unenrolled for {activity}").format(activity=participation.event.summary) participation_transaction(participation, reason, cancel=True, added_by=added_by) @@ -229,3 +237,66 @@ def free_cookies_sale(article, amount, person, added_by): discount=discount, article=article, amount=amount, added_by=added_by) transaction.save() return transaction + + +def get_transaction_payment_status(transaction: Optional[Transaction], mandate_type: Optional[str] = None) -> Tuple[str, Optional[Union[PersonalTabSettlement, ReversalTransaction, Authorization]]]: + """ + Returns information on how the given transaction was paid, or how it might probably be paid in the future. + + If the transaction is None, it returns ('no_transaction', None) + If the transaction is paid, returns ('paid', ). + If the transaction is not paid: + - If the transaction was reversed, it returns ('reversed', ). + - If the transaction was not reversed: + - If the person has a direct debit mandate, returns ('unpaid', ). + - If the person has no mandate, it returns ('unpaid', None). + """ + # Short-circuit if no transaction was given + if not transaction: + return 'no_transaction', None # No transaction! + + # If the participation is paid, returns the settlement in which it was paid. + if transaction.is_paid(): + settlement_type = transaction.settlement.settlement_type + if settlement_type == "INSTR" and hasattr(transaction.settlement, 'debtcollectioninstruction'): + return 'paid', transaction.settlement.debtcollectioninstruction + elif settlement_type == "MANUAL" and hasattr(transaction.settlement, 'manualpaymentsettlement'): + return 'paid', transaction.settlement.manualpaymentsettlement + return 'paid', transaction.settlement + + # If the participation is not paid: + else: + # If the transaction was reversed, it returns the ReversalTransaction. + if transaction.settlement and transaction.settlement.settlement_type == "INSTR" and hasattr(transaction.settlement, 'debtcollectioninstruction') and hasattr(transaction.settlement.debtcollectioninstruction, 'reversal'): + return 'reversed', transaction.settlement.debtcollectioninstruction.reversal.transaction + # If the transaction was not reversed, returns the direct debit mandate if the person has one, else None. + return 'unpaid', transaction.person.has_mandate(mandate_type=mandate_type).order_by('authorization_type__id').first() # Returns None if no authorization exists. + + +def get_personal_tab_balances(only_members: bool = False, only_without_mandate: bool = True) -> List[Dict[str, Any]]: + """ + Gets a list of dictionaries, containing the personal tab balances of all people/members. + """ + people = Person.objects.all() + if only_members: + people = people.filter(Q(membership__ended__isnull=True) | Q(membership__ended__gt=datetime.date.today()), membership__year=current_association_year()).distinct() + if only_without_mandate: + people = people.exclude(authorization__is_signed=True, authorization__end_date__isnull=True, authorization__authorization_type__consumptions=True).distinct() + + results = [] + now = timezone.now() + + for person in people: + person_transactions = Transaction.objects.filter(person=person, date__lt=now) + unpaid_transactions = person_transactions.filter(settlement=None) + curr_balance = person_transactions.aggregate(Sum('price'))['price__sum'] or Decimal('0.00') + results.append({ + 'person': person, + 'balance': curr_balance, + 'transactions': person_transactions, + 'unpaid_transactions': unpaid_transactions, + 'unpaid_sum': unpaid_transactions.aggregate(Sum('price'))['price__sum'], + }) + + return results + diff --git a/amelie/personal_tab/urls.py b/amelie/personal_tab/urls.py index 3e1260a5..8bfc1885 100644 --- a/amelie/personal_tab/urls.py +++ b/amelie/personal_tab/urls.py @@ -20,6 +20,7 @@ path('me/', views.my_dashboard, name='my_dashboard'), path('transactions/', views.transaction_form, name='transactions'), + path('transactions/unpaid/', views.UnpaidTransactionsListView.as_view(), name='unpaid_transactions_list'), path('transactions///', views.transaction_overview, name='transactions'), path('transactions/activity//', ActivityTransactionDetail.as_view(), name='activity_transaction_detail'), @@ -32,7 +33,7 @@ name='cookie_corner_transaction_delete'), path('unpaid_memberships/', views.unpaid_memberships, name='unpaid_memberships'), - path('unpaid_memberships//', views.unpaid_memberships, name='unpaid_memberships_year'), + path('unpaid_memberships//', views.unpaid_memberships_year, name='unpaid_memberships_year'), path('unpaid_memberships//mailing/', views.unpaid_memberships_mailing, name='unpaid_memberships_mailing'), path('unpaid_memberships//forgive/', views.unpaid_memberships_forgive, name='unpaid_memberships_forgive'), @@ -52,8 +53,8 @@ path('person///', views.dashboard, name='dashboard'), path('person///transactions/', views.person_transactions, name='person_transactions'), - path('person///transactions///', - views.person_transactions, name='person_transactions'), + path('person///transactions///', views.person_transactions, name='person_transactions'), + path('person///transactions/unpaid/', views.person_transactions_unpaid, name='person_transactions_unpaid'), path('person///new//', views.person_new_transaction, name='person_new_transaction'), @@ -67,6 +68,12 @@ path('person///exam_cookie_credit/new/', views.person_exam_cookie_credit_new, name='person_exam_cookie_credit_new'), + path('manual_payments/', views.ManualPaymentsListView.as_view(), name='manual_payments_list'), + path('person///manual_payments/', views.person_manual_payments, name='person_manual_payments'), + path('person///manual_payments/new/', views.CreateManualPaymentSettlementView.as_view(), name='person_manual_payment_new'), + path('manual_payments//', views.manual_payment_settlement_view, name='manual_payment_settlement_view'), + path('manual_payments//delete/', views.DeleteManualPaymentSettlementView.as_view(), name='manual_payment_settlement_delete'), + path('rfid//edit//', views.rfid_change_status, name='rfid_change_status'), path('rfid//remove/', views.rfid_remove, name='rfid_remove'), diff --git a/amelie/personal_tab/views.py b/amelie/personal_tab/views.py index 58f41475..c2444aad 100644 --- a/amelie/personal_tab/views.py +++ b/amelie/personal_tab/views.py @@ -1,5 +1,6 @@ # coding=utf-8 import datetime +from collections import OrderedDict from datetime import timezone as tz import logging from decimal import Decimal @@ -7,43 +8,48 @@ import traceback import operator from functools import reduce +from typing import List, Dict import django.conf from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.urls import reverse, reverse_lazy -from django.utils.translation import get_language, gettext_lazy as _l +from django.utils.translation import gettext_lazy as _l from django.db import transaction from django.db.models import Sum, Q, Count from django.db.models.functions import TruncDay -from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseForbidden +from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseForbidden, HttpResponseNotAllowed from django.shortcuts import render, get_object_or_404, redirect -from django.utils import formats, timezone +from django.utils import formats, timezone, translation from django.utils.decorators import method_decorator -from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, get_language +from django.views.generic import ListView from django.views.generic.detail import DetailView -from django.views.generic.edit import UpdateView, DeleteView +from django.views.generic.edit import UpdateView, DeleteView, CreateView from django.views.generic.edit import FormView from amelie.calendar.models import Event -from amelie.members.models import MembershipType, Payment, PaymentType, Person, Membership +from amelie.members.models import MembershipType, Person, Membership from amelie.members.query_forms import MailingForm from amelie.settings.generic import DATE_PRE_SEPA_AUTHORIZATIONS from amelie.personal_tab.alexia import get_alexia, parse_datetime from amelie.personal_tab.helpers import kcal_equivalent from amelie.personal_tab.forms import CookieCornerTransactionForm, CustomTransactionForm, ExamCookieCreditForm, \ - DebtCollectionForm, ReversalForm, SearchAuthorizationForm, AmendmentForm, DebtCollectionBatchForm, AuthorizationSelectForm, \ - StatisticsForm, DeclarationForm + DebtCollectionForm, ReversalForm, SearchAuthorizationForm, AmendmentForm, DebtCollectionBatchForm, \ + AuthorizationSelectForm, \ + StatisticsForm, DeclarationForm, ManualPaymentSettlementForm, ManualPaymentTransactionFormSet from amelie.personal_tab.debt_collection import delete_amendment, delete_reversal, edit_amendment, edit_reversal, generate_contribution_instructions, filter_contribution_instructions, \ save_contribution_instructions, generate_cookie_corner_instructions, filter_cookie_corner_instructions, save_cookie_corner_instructions, \ process_reversal, process_amendment -from amelie.personal_tab.models import Amendment, Category, Declaration, Transaction, CookieCornerTransaction, ActivityTransaction, \ - CustomTransaction, AlexiaTransaction, RFIDCard, Authorization, DebtCollectionAssignment, DebtCollectionBatch, DiscountCredit, \ - DebtCollectionInstruction, ReversalTransaction +from amelie.personal_tab.models import Amendment, Category, Declaration, Transaction, CookieCornerTransaction, \ + ActivityTransaction, \ + CustomTransaction, AlexiaTransaction, RFIDCard, Authorization, DebtCollectionAssignment, DebtCollectionBatch, \ + DiscountCredit, \ + DebtCollectionInstruction, ReversalTransaction, ManualPaymentSettlement, SettlementManualPaymentTransaction, \ + SettlementExtraBalanceTransaction, DebtCollectionTransaction, ContributionTransaction, PaymentMethod from amelie.personal_tab.statistics import get_functions, statistics_totals from amelie.personal_tab.transactions import exam_cookie_discount, \ exam_cookie_credit as transactions_exam_cookie_credit, add_exam_cookie_credit @@ -115,7 +121,8 @@ def generate_overview(request, person, date_from=None, date_to=None): start_url = _urlize(date_from) if date_from else None end_url = _urlize(date_to) if date_to else None - transaction_types = [CookieCornerTransaction, ActivityTransaction, AlexiaTransaction, CustomTransaction] + transaction_types = [CookieCornerTransaction, ActivityTransaction, AlexiaTransaction, ContributionTransaction, CustomTransaction] + payment_transaction_types = [DebtCollectionTransaction, ReversalTransaction, SettlementManualPaymentTransaction, SettlementExtraBalanceTransaction] transaction_filter = [] if date_from or date_to: transaction_filter.append(Q(date__gte=date_from, date__lt=date_to)) @@ -134,8 +141,10 @@ def generate_overview(request, person, date_from=None, date_to=None): overview_type = None total = False totals = None + all_payment_totals = None kcal_totals = None all_transactions = None + payment_transactions = None rows = None if has_transactions: @@ -157,20 +166,22 @@ def generate_overview(request, person, date_from=None, date_to=None): # Execute query if len(transaction_filter) > 0: all_transactions = [x.objects.filter(reduce(operator.and_, transaction_filter)) for x in transaction_types] + payment_transactions = [x.objects.filter(reduce(operator.and_, transaction_filter)) for x in payment_transaction_types] else: all_transactions = [x.objects.all() for x in transaction_types] + payment_transactions = [x.objects.all() for x in payment_transaction_types] if overview_type == 'day': all_transactions[0] = all_transactions[0].select_related('person', 'article', 'discount') all_transactions[1] = all_transactions[1].select_related('person', 'event') all_transactions[2] = all_transactions[2].select_related('person') all_transactions[3] = all_transactions[3].select_related('person') + all_transactions[4] = all_transactions[4].select_related('person') - # On total page show the latest 10 transactions - # TODO: variable does not seem to be used? - albertskja 26-04-2018 - if not date_from: - # Table with latest 10 transactions - latest_10_transactions = _trans.order_by('-added_on')[:10] + payment_transactions[0] = payment_transactions[0].select_related('person') + payment_transactions[1] = payment_transactions[1].select_related('person') + payment_transactions[2] = payment_transactions[2].select_related('person') + payment_transactions[3] = payment_transactions[3].select_related('person') # Generate table with data if overview_type != 'day': @@ -206,13 +217,20 @@ def _datetime_end(t): for trans in itertools.chain.from_iterable(all_transactions): data.add(_datetime_start(trans.date.astimezone(localtz))) + for trans in itertools.chain.from_iterable(payment_transactions): + data.add(_datetime_start(trans.date.astimezone(localtz))) for date in sorted(data): amounts = [ - x.filter(date__gte=date, date__lt=_datetime_end(date)).aggregate(Sum('price'))['price__sum'] or 0 - for x in all_transactions] - - # Add the total to the end + x.filter(date__gte=date, date__lt=_datetime_end(date)).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") + for x in all_transactions + ] + payment_totals = [ + x.filter(date__gte=date, date__lt=_datetime_end(date)).aggregate(Sum('price'))['price__sum'] or Decimal("0.00") + for x in payment_transactions + ] + + # Add the totals to the end amounts.append(sum(amounts)) # TODO: BLEGH, custom SQL! - albertskja 2018-04-25 @@ -229,13 +247,15 @@ def _datetime_end(t): rows.append({ 'date': date, 'amounts': amounts, + 'payment_totals': sum(payment_totals), 'kcal_total': kcal_total, 'start_url': start_url_d, 'end_url': end_url_d }) # Calculate totals - totals = [x.aggregate(Sum('price'))['price__sum'] or 0 for x in all_transactions] + totals = [x.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") for x in all_transactions] + all_payment_totals = [x.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") for x in payment_transactions] # Add totals to the end totals.append(sum(totals)) @@ -255,11 +275,14 @@ def _datetime_end(t): 'form': form, 'date_from': date_from, 'date_to': date_to, + 'date_now': datetime.datetime.now(tz.utc), 'has_transactions': has_transactions, 'all_transactions': all_transactions, + 'payment_transactions': payment_transactions, 'overview_type': overview_type, 'total': total, 'totals': totals, + 'payment_totals': sum(all_payment_totals), 'kcal_totals': kcal_totals, 'rows': rows, 'view_name': view_name @@ -468,7 +491,7 @@ def transaction_form(request): start = form.cleaned_data['datetime_from'].astimezone(tz.utc) end = form.cleaned_data['datetime_to'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:transactions', args=[_urlize(start), _urlize(end)])) - + else: return render(request, 'cookie_corner_transactions_form.html', { 'form': form, @@ -496,50 +519,52 @@ def transaction_overview(request, date_from, date_to): end = _parsedatetime(date_to) except ValueError: raise Http404(_('Invalid date`')) - + return generate_overview(request, None, start, end) @require_board -def unpaid_memberships(request, year=None): - """Give an overview of unpaid memberships per year or, if a year is specified, per type.""" - - # If no year is given, show overview of all years with unpaid memberships - if year is None: - unpaid_memberships = Membership.objects.filter(payment__isnull=True, type__price__gt=0) - membership_types = [(mt.name, mt.pk) for mt in MembershipType.objects.filter(membership__payment__isnull=True, price__gt=0).distinct()] - years = unpaid_memberships.values('year').distinct().order_by('-year') - price = unpaid_memberships.values('type__price', 'year').order_by('year') - - - # Amount totals looks like this: - # {'2025' : ({'Primary Yearlong' : 47, 'Secondary Yearlong' : 47}, 470.00), '2024' : ...} - totals = {} - for year_total in years: - totals[year_total['year']] = ({}, sum(item['type__price'] for item in price if item['year'] == year_total['year'])) - for membership_type in membership_types: - totals[year_total['year']][0][membership_type[0]] = unpaid_memberships.filter(type=membership_type[1], year=year_total['year']).count() - - return render(request, 'unpaid_memberships/overview.html', {'totals': totals, 'membership_types': membership_types}) - - - # If a year is given, show the unpaid memberships for that year, grouped by membership type - unpaid_memberships = Membership.objects.filter(payment__isnull=True, type__price__gt=0, year=year).select_related('member').order_by('type', 'member__first_name') - +def unpaid_memberships(request): + """Give an overview of unpaid memberships per year.""" + # For memberships specifically, checking for the existence of a ContributionTransaction without settlement + # is enough to see if it is unpaid, because even if a contribution direct debit is cancelled, it will create + # a new ContributionTransaction without a settlement so it can be debited again later. + unpaid_memberships = Membership.objects.filter(contributiontransaction__settlement=None, type__price__gt=0).distinct() + membership_types = [(mt.name, mt.pk) for mt in MembershipType.objects.filter(membership__contributiontransaction__settlement=None, price__gt=0).distinct()] + years = unpaid_memberships.values('year').distinct().order_by('-year') + price = unpaid_memberships.values('type__price', 'year').order_by('year') + + # Amount totals looks like this: + # {'2025': ({'Primary Yearlong': 47, 'Secondary Yearlong': 47}, 470.00), '2024': ...} + totals = {} + for year_total in years: + totals[year_total['year']] = ({}, unpaid_memberships.aggregate(price_sum=Sum('type__price', filter=Q(year=year_total['year'])))['price_sum']) + for membership_type in membership_types: + totals[year_total['year']][0][membership_type[0]] = unpaid_memberships.filter(type=membership_type[1], year=year_total['year']).count() + + return render(request, 'unpaid_memberships/overview.html', {'totals': totals, 'membership_types': membership_types}) + +@require_board +def unpaid_memberships_year(request, year): + """Give an overview of unpaid memberships for a year, grouped by membership type.""" + unpaid = Membership.objects.filter( + contributiontransaction__settlement=None, type__price__gt=0, year=year + ).distinct().select_related('member').order_by('type', 'member__first_name') + grouped = {} - for membership in unpaid_memberships: + for membership in unpaid: membership_type = (membership.type.name, membership.type.pk) if membership_type not in grouped: grouped[membership_type] = [] grouped[membership_type].append(membership) - + return render(request, 'unpaid_memberships/year_overview.html', {'unpaid_memberships': grouped, 'year': year}) @require_board def unpaid_memberships_forgive(request, year): """Forgive the membership fee of persons that were selected in the unpaid memberships overview.""" - + # Get the selected memberships from the URL parameters selected_memberships = request.GET.get('memberships') if not selected_memberships: @@ -556,18 +581,42 @@ def unpaid_memberships_forgive(request, year): if request.method == "POST": if 'confirm' in request.POST: - # Payment Type 12 is the "Forgiven" payment type - forgiven_payment_type = PaymentType.objects.get(pk=12) - for membership in memberships: - # Create a Payment object for the membership with the "Forgiven" payment type - payment = Payment( - membership=membership, - amount=membership.type.price, - date=timezone.now(), - payment_type=forgiven_payment_type, - ) - payment.save() - messages.success(request, _("The membership fees have been forgiven.")) + with transaction.atomic(): + # Get the payment method for "Forgiven" (configured in settings) + payment_method = PaymentMethod.objects.get(pk=settings.MEMBERS_FORGIVEN_PAYMENT_METHOD_ID) + num_forgiven = 0 + + for membership in memberships: + # Skip any memberships that are already paid (we shouldn't encounter any but just to be safe) + if membership.is_paid(): + continue + + # Create a settlement for all open ContributionTransactions of this membership, + transactions = [t for t in membership.contributiontransaction_set.all() if not t.is_paid()] + + # Create a ManualPaymentSettlement with the selected payment method + if transactions: + # Translate the description to the user's preferred language + with translation.override(membership.member.preferred_language): + settlement_description = _( + "Forgiven membership costs for {membership_type} ({begin_year}/{end_year})").format( + membership_type=membership.type.name, + begin_year=membership.year, + end_year=(membership.year + 1) + ) + ManualPaymentSettlement.create_for_transactions( + transactions=transactions, + payment_method=payment_method, + person=membership.member, + settlement_description=settlement_description, + payment_description=settlement_description, + created_by=request.person, + ) + num_forgiven += 1 + else: + raise ValueError(f"Tried to forgive payment for unpaid membership {membership.pk} but no transactions exist!") + + messages.success(request, _("The membership fees for {amount} memberships have been forgiven.").format(amount=num_forgiven)) return redirect('personal_tab:unpaid_memberships_year', year) else: @@ -620,7 +669,7 @@ def unpaid_memberships_mailing(request, year): * Bedrag: € {{{{membership.price}}}} Cash of PIN betaling is ook mogelijk door fysiek langs te komen bij de verenigingskamer. - + Met vriendelijke groet, {} @@ -700,7 +749,6 @@ def unpaid_memberships_mailing(request, year): }) - class TransactionSecurityMixin(RequirePersonMixin): """ Mixin for SingleObjectMixin and MultipleObjectMixin to select only transactions @@ -805,7 +853,7 @@ def form_valid(self, form): object = self.get_object() if object.discount: object.discount.delete() - + delete = super(CustomTransactionDelete, self).delete(self.request) messages.success(self.request, _("The transaction '{transaction}' has been successfully deleted.") .format(transaction=self.object)) @@ -834,15 +882,11 @@ def dashboard(request, pk, slug): personal_transactions = Transaction.objects.filter(person=person).order_by('-added_on')[:5] - # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) now = timezone.now() today = now.astimezone(timezone.get_default_timezone()).date() - curr_balance = Transaction.objects.filter(person=person, date__gte=begin, date__lt=now).aggregate( - Sum('price'))['price__sum'] or Decimal('0.00') - all_balance = Transaction.objects.filter(person=person, date__gte=begin).aggregate( - Sum('price'))['price__sum'] or Decimal('0.00') + curr_balance = Transaction.objects.filter(person=person, date__lt=now).aggregate(Sum('price'))['price__sum'] or Decimal('0.00') + all_balance = Transaction.objects.filter(person=person).aggregate(Sum('price'))['price__sum'] or Decimal('0.00') future_debt_collection_instructions = DebtCollectionInstruction.objects.filter(authorization__person=person, batch__execution_date__gt=today) @@ -850,6 +894,7 @@ def dashboard(request, pk, slug): exam_cookie_credits = transactions_exam_cookie_credit(person) debt_collection_instructions = DebtCollectionInstruction.objects.filter(authorization__person=person ).order_by('-batch__execution_date')[:5] + manual_payment_settlements = ManualPaymentSettlement.objects.filter(person=person)[:5] # Date on which the old (pre-SEPA) authorizations are registered date_old_authorizations = DATE_PRE_SEPA_AUTHORIZATIONS @@ -861,6 +906,7 @@ def dashboard(request, pk, slug): 'all_balance': all_balance, 'future_debt_collection_instructions': future_debt_collection_instructions, 'debt_collection_instructions': debt_collection_instructions, + 'manual_payment_settlements': manual_payment_settlements, 'exam_cookie_credits': exam_cookie_credits, 'date_old_authorizations': date_old_authorizations, 'wrapped_year': django.conf.settings.COOKIE_CORNER_WRAPPED_YEAR @@ -907,6 +953,82 @@ def person_transactions(request, pk, slug, date_from=None, date_to=None): return generate_overview(request, person, start, end) +@require_lid +def person_transactions_unpaid(request, pk, slug): + person = get_object_or_404(Person, pk=pk, slug=slug) + + # Only the person themselves or the board has access to this. + if not request.person == person and not request.is_board: + return HttpResponseForbidden() + + transaction_types = [ + # (TransactionType, ['related', 'objects', 'to_select'], "Display Name") + (CookieCornerTransaction, ['person', 'article', 'discount'], _l("Cookie Corner Transaction")), + (ActivityTransaction, ['person', 'event'], _l("Activity Transaction")), + (AlexiaTransaction, ['person'], _l("Alexia Transaction")), + (ContributionTransaction, ['person'], _l("Contribution Transaction")), + (CustomTransaction, ['person'], _l("Custom Transaction")), + ] + payment_transaction_types = [ + # (TransactionType, ['related', 'objects', 'to_select'], "Display Name") + (DebtCollectionTransaction, ['person'], _l("Debt Collection Transaction")), + (ReversalTransaction, ['person'], _l("Reversal Transaction")), + (SettlementManualPaymentTransaction, ['person'], _l("Manual Payment Transaction")), + (SettlementExtraBalanceTransaction, ['person'], _l("Extra Balance Transaction")), + ] + + _trans = Transaction.objects.filter(person=person, settlement=None) + has_transactions = _trans.exists() + + # Vars for template context that might not be initialized sometimes + total = False + totals = [] + all_payment_totals = [] + transactions_total = Decimal('0.00') + payment_total = [] + all_transactions = None + payment_transactions = None + + # Person's balances + now = timezone.now() + curr_balance = Transaction.objects.filter(person=person, date__lt=now).aggregate(Sum('price'))['price__sum'] or Decimal('0.00') + all_balance = Transaction.objects.filter(person=person).aggregate(Sum('price'))['price__sum'] or Decimal('0.00') + + if has_transactions: + total = True # Generate totals + + # Get transactions and their related objects per type + all_transactions = [x[0].objects.filter(person=person, settlement=None).select_related(*x[1]) for x in transaction_types] + payment_transactions = [x[0].objects.filter(person=person, settlement=None).select_related(*x[1]) for x in payment_transaction_types] + + # Calculate totals + totals = [x.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") for x in all_transactions] + all_payment_totals = [x.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") for x in payment_transactions] + + transactions_total = sum(totals) + payment_total = sum(all_payment_totals) + + # Add totals to the end + totals.append(transactions_total) + + sums_per_type = [(tt[2], totals[i]) for i, tt in enumerate(transaction_types) if totals[i] != 0] + + # Done! + return render(request, 'cookie_corner_unpaid_transactions.html', { + 'person': person, + 'has_transactions': has_transactions, + 'all_transactions': all_transactions, + 'payment_transactions': payment_transactions, + 'total': total, + 'totals': totals, + 'payment_totals': payment_total, + 'curr_balance': curr_balance, + 'all_balance': all_balance, + 'sums_per_type': sums_per_type, + 'sums_balance': transactions_total + payment_total, + }) + + @require_board def person_new_transaction(request, person_id, slug, transaction_type): person = get_object_or_404(Person, id=person_id, slug=slug) @@ -946,6 +1068,302 @@ def person_debt_collection_instructions(request, person_id, slug): }) +class ManualPaymentsListView(ListView): + model = ManualPaymentSettlement + template_name = 'cookie_corner_manual_payments_list.html' + paginate_by = 100 + + +class UnpaidTransactionsListView(ListView): + model = Person + template_name = 'cookie_corner_unpaid_transactions_list.html' + paginate_by = 50 + + def get_context_data(self, *args, **kwargs): + context = super(UnpaidTransactionsListView, self).get_context_data(*args, **kwargs) + context['filter_no_mandate'] = self.request.GET.get('filter_no_mandate', False) + context['filter_only_mandate'] = self.request.GET.get('filter_only_mandate', False) + context['filter_mandate_type'] = self.request.GET.get('filter_mandate_type', None) + if context['filter_mandate_type'] not in ['contribution', 'consumptions']: + context['filter_mandate_type'] = None + context['filter_transactions'] = self.request.GET.get('filter_transactions', None) + + qs = self.get_queryset() + # Get total counts for in the summary table at the top of the page + filter_transaction_type = self.request.GET.get('filter_transactions', '').lower() + if filter_transaction_type not in ['activity', 'alexia', 'contribution', 'cookiecorner', 'custom', 'reversal']: + filter_transaction_type = None + context['person_count'] = qs.count() + context['transaction_count'] = sum(person.unpaid_transactions(transaction_type=filter_transaction_type).count() for person in qs) + context['total_costs'] = sum(person.unpaid_transactions_total_cost(transaction_type=filter_transaction_type) for person in qs) + return context + + def get_queryset(self): + qs = Person.objects.all().prefetch_related('transaction_set') + + ## + # Selectable filters + ## + + # Mandate filters + if self.request.GET.get('filter_no_mandate', '').lower() in ['true', 't', '1']: + qs = qs.exclude(authorization__is_signed=True, authorization__end_date__isnull=True).distinct() + if self.request.GET.get('filter_only_mandate', '').lower() in ['true', 't', '1']: + qs = qs.filter(authorization__is_signed=True, authorization__end_date__isnull=True).distinct() + + # Mandate type filters + filter_mandate_type = self.request.GET.get('filter_mandate_type', '').lower() + if filter_mandate_type == 'contribution': + qs = qs.filter(authorization__authorization_type__contribution=True).distinct() + elif filter_mandate_type == 'consumptions': + qs = qs.filter(authorization__authorization_type__consumptions=True).distinct() + + # Transaction type filters + filter_transaction_type = self.request.GET.get('filter_transactions', '').lower() + if filter_transaction_type in ['activity', 'alexia', 'contribution', 'cookiecorner', 'custom', 'reversal']: + qs = qs.filter(**{f'transaction__{filter_transaction_type}transaction__isnull': False, f'transaction__{filter_transaction_type}transaction__settlement': None}) + + # Actual transaction filter + qs = qs.filter( + transaction__isnull=False, transaction__settlement=None + ).distinct() + + return qs + + +@login_required +def person_manual_payments(request, person_id, slug): + person = get_object_or_404(Person, id=person_id, slug=slug) + manual_payments = ManualPaymentSettlement.objects.filter(person=person) + + # Done + return render(request, 'cookie_corner_person_manual_payments.html', { + 'person': person, + 'manual_payments': manual_payments + }) + + +@login_required +def manual_payment_settlement_view(request, id): + settlement = get_object_or_404(ManualPaymentSettlement, id=id) + if not request.is_board and request.person != settlement.person: + raise PermissionDenied + + manual_payment_transaction = settlement.manual_payment_transaction + extra_balance_transaction = settlement.extra_balance_transaction + settlement_transactions = settlement.transactions.all() + if manual_payment_transaction: + settlement_transactions = settlement_transactions.exclude(pk=manual_payment_transaction.pk) + + categorized_transactions: List[Dict] = [] + if settlement_transactions: + # First, show all regular settlement transactions + categorized_transactions.append({ + 'title': _l("Personal tab transactions in this settlement"), # No title for this category + 'transactions': settlement_transactions, + 'show_total': True, + 'totals': settlement_transactions.aggregate(Sum('price'))['price__sum'] or Decimal("0.00") + }) + # Then show the manual payment that was created to settle those (if present) + if manual_payment_transaction: + categorized_transactions.append({ + 'title': _l("Payment for the transactions above"), + 'transactions': [manual_payment_transaction], + 'show_total': False, + 'totals': manual_payment_transaction.price + }) + # Then show the balance that was settled with the personal tab (if present) + if extra_balance_transaction: + if extra_balance_transaction.price > 0: + extra_balance_title = _l("Residual balance debited from personal tab") + else: + extra_balance_title = _l("Residual balance credited to personal tab") + categorized_transactions.append({ + 'title': extra_balance_title, + 'transactions': [extra_balance_transaction], + 'show_total': False, + 'totals': extra_balance_transaction.price + }) + + return render(request, 'cookie_corner_manual_payment_settlement_view.html', { + 'settlement': settlement, + 'transactions': categorized_transactions, + 'show_total': True, + 'totals': sum(c['totals'] for c in categorized_transactions), + }) + + +class DeleteManualPaymentSettlementView(DeleteView): + model = ManualPaymentSettlement + template_name = 'cookie_corner_person_manual_payment_confirm_delete.html' + + object: ManualPaymentSettlement + + def get_object(self, *args, **kwargs): + obj: ManualPaymentSettlement = super().get_object(*args, **kwargs) + can_delete, reason = obj.deletion_possible() + if not can_delete: + raise ValueError(reason) + return obj + + def get_success_url(self): + return reverse('personal_tab:dashboard', kwargs={'pk': self.object.person.pk, 'slug': self.object.person.slug}) + + def delete(self, request, *args, **kwargs): + # Only allow deletion via POST form submission on confirmation page. + return HttpResponseNotAllowed(["GET", "POST"]) + + def form_valid(self, form): + success_url = self.get_success_url() + with transaction.atomic(): + # First, we need to unlink all the transactions that are in some way linked to this settlement. + # This avoids triggering the PROTECT setting on the foreign key relation when deleting the settlement. + + # Then, all regular (other) transactions need to be unlinked from the settlement. + for t in self.object.transactions.all(): + t.settlement = None + t.save() + + # Delete the settlement. + self.object.delete() + + # If the settlement had a SettlementManualPaymentTransaction, it needs to be deleted as the settlement will be undone. + if self.object.manual_payment_transaction: + self.object.manual_payment_transaction.delete() + + # If the settlement had a SettlementExtraBalanceTransaction, it needs to be deleted as the extra payment will be undone. + if self.object.extra_balance_transaction: + self.object.extra_balance_transaction.delete() + + messages.success( + self.request, + _("Manual payment '{payment}' of '{person}' deleted.").format( + payment=str(self.object), person=str(self.object.person.incomplete_name()) + ) + ) + return HttpResponseRedirect(success_url) + + +class CreateManualPaymentSettlementView(CreateView): + model = ManualPaymentSettlement + form_class = ManualPaymentSettlementForm + template_name = 'cookie_corner_person_manual_payment_create.html' + + def __init__(self, *args, **kwargs): + self.object = None + super().__init__(*args, **kwargs) + + def dispatch(self, request, *args, **kwargs): + self.person = Person.objects.get(pk=self.kwargs['person_id']) + return super().dispatch(request=request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(CreateManualPaymentSettlementView, self).get_context_data(**kwargs) + + context['person'] = self.person + person_unpaid_transactions = [ + {'transaction': t, 'include': False} + for t in Transaction.objects.filter(person=context['person'], settlement=None) + ] + context['transaction_formset'] = ManualPaymentTransactionFormSet(self.request.POST or None, initial=person_unpaid_transactions) + context['date_now'] = datetime.datetime.now(tz.utc) + # Current personal tab balances (in cents) + context['balance'] = { + 'today': (Transaction.objects.filter(person=self.person, date__lt=timezone.now()).aggregate(Sum('price'))['price__sum'] or Decimal('0.00')) * 100, + 'all': (Transaction.objects.filter(person=self.person).aggregate(Sum('price'))['price__sum'] or Decimal('0.00')) * 100 + } + return context + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs.update({ + 'person': Person.objects.get(pk=self.kwargs['person_id']) + }) + return kwargs + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + form = self.get_form() # ManualPaymentSettlement metadata + transaction_formset = ManualPaymentTransactionFormSet(self.request.POST) # Selected transactions + if form.is_valid() and transaction_formset.is_valid(): + try: + return self.form_valid(form, transaction_formset) + except ValidationError as e: + # Could be thrown if no transactions were selected and no extra money is deposited (empty payment) + form.add_error(None, e) + return self.form_invalid(form) + else: + return self.form_invalid(form) + + @transaction.atomic + def form_valid(self, form: ManualPaymentSettlementForm, transaction_formset): + payment_date: datetime.date = form.cleaned_data.get('payment_date') + payment_method: PaymentMethod = form.cleaned_data.get('payment_method') + paid_amount: Decimal = form.cleaned_data.get('paid_amount') or Decimal("0.00") + + # Get datetime for settlement and transactions + timezone_amsterdam = timezone.get_default_timezone() + transaction_datetime = datetime.datetime.combine(payment_date, datetime.time(0, 0)).replace(tzinfo=timezone_amsterdam) + + # Get transactions + transaction_candidates = Transaction.objects.filter(person=self.person, settlement=None) + transaction_pks = [t.get('id') for t in transaction_formset.cleaned_data if t.get('include', False)] + transactions = transaction_candidates.filter(id__in=transaction_pks) + + # Calculate the extra amount paid (transaction sum - amount paid) + total_transaction_price = sum(t.price for t in transactions) or Decimal("0.00") + extra_amount = paid_amount - total_transaction_price + + # Get description strings in the person's preferred language + with translation.override(self.person.preferred_language): + settlement_description = _("Personal tab settlement on {date} for {name}").format( + date=payment_date, name=self.person.incomplete_name() + ) + if paid_amount < 0: + # Negative payment, money goes from association to person + payment_description = _("Refund on {date} for {name}").format( + date=payment_date, name=self.person.incomplete_name() + ) + else: + # Positive (or zero) payment, money goes from person to association + payment_description = _("Manual payment on {date} for {name}").format( + date=payment_date, name=self.person.incomplete_name() + ) + + if extra_amount < 0: + # Personal tab balance will need to be used to settle these transactions (less paid than transaction sum) + balance_description = _("Debit from personal tab balance on {date} for {name}").format( + date=payment_date, name=self.person.incomplete_name() + ) + else: + # Personal tab balance will be added after settling these transactions (more paid than transaction sum) + balance_description = _("Credit to personal tab balance on {date} for {name}").format( + date=payment_date, name=self.person.incomplete_name() + ) + + settlement = ManualPaymentSettlement.create_for_transactions( + transactions=transactions, + payment_method=payment_method, + person=self.person, + settlement_description=settlement_description, + payment_description=payment_description, + leftover_balance_description=balance_description, + paid_amount=paid_amount, + payment_datetime=transaction_datetime, + created_by=self.request.person, + ) + + if settlement: + # Redirect to the settlement detail page. + return redirect(settlement) + else: + messages.error(self.request, _("No settlement was created because no transactions were selected and no amount was paid.")) + return redirect(reverse('personal_tab:dashboard', kwargs={'pk': self.person.pk, 'slug': self.person.slug})) + + @require_board def exam_cookie_credit(request, date_from=False, date_to=False): """Give an overview of exam cookie credits.""" @@ -1114,10 +1532,7 @@ def balance(request, dt_str=False): dt_url = _urlize(dt) - # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) - - all_transactions = Transaction.objects.filter(date__gte=begin, date__lt=dt) + all_transactions = Transaction.objects.filter(date__lt=dt) all_transactions_aggregated = all_transactions.aggregate(Sum('price')) all_transactions_sum = all_transactions_aggregated['price__sum'] @@ -1548,7 +1963,7 @@ def debt_collection_new(request): date_text = formats.date_format(end_date) form = DebtCollectionForm(minimal_execution_date, initial={'description': 'Cookie corner until {}'.format(date_text), - 'contribution': False, 'cookie_corner': True, 'end': end_date, + 'contribution': False, 'cookie_corner': True, 'end': end_date, 'contribution_years': current_association_year()}) return render(request, 'debt_collection/new.html', { @@ -1769,7 +2184,7 @@ def _get_person_context_contribution(instruction): """ person, context = _get_person_context(instruction) - membership = Membership.objects.get(contributiontransaction__debt_collection=instruction) + membership = Membership.objects.get(contributiontransaction__settlement=instruction) context['membership'] = { 'type': membership.type.name } @@ -2067,7 +2482,7 @@ def cookie_corner_wrapped_main(request, year=None): products_grouped_by_count[article]["count"] += amount else: products_grouped_by_count[article] = {"count": amount} - + products_grouped_by_count = sorted(products_grouped_by_count.items(), key=lambda x:x[1]["count"]) products_grouped_by_count.reverse() @@ -2246,9 +2661,9 @@ def cookie_corner_wrapped_global(request, year=None): class DeclarationView(RequirePersonMixin, FormView): - """ + """ Form view for submitting a declarations via the website. - + Only available to logged in (former) members. """ @@ -2272,7 +2687,7 @@ def form_valid(self, form): try: form.save(request=self.request) messages.success(self.request, _("Declaration was submitted successfully.")) - + except Exception as e: trace = traceback.format_exc() logging.error(f"Error while submitting declaration: {str(e.__class__.__name__)} - {trace}") @@ -2282,9 +2697,9 @@ def form_valid(self, form): @require_board def declaration_pdf(request, declaration_id): - """ + """ View for generating the PDF of a declaration. - + Only available to board members. """ declaration = get_object_or_404(Declaration, id=declaration_id) diff --git a/amelie/settings/generic.py b/amelie/settings/generic.py index a702998c..96ab5704 100644 --- a/amelie/settings/generic.py +++ b/amelie/settings/generic.py @@ -74,6 +74,11 @@ # The direct debit debtor ID of Inter-Actief that needs to be printed on the autorization forms DIRECT_DEBIT_DEBTOR_ID = 'NL81ZZZ400749470000' +# The ID of the personal_tab.PaymentMethod object representing an early termination (no payment needed). +MEMBERS_EARLY_TERMINATION_PAYMENT_METHOD_ID = 10 +# The ID of the personal_tab.PaymentMethod object representing a Forgiven payment (no payment needed). +MEMBERS_FORGIVEN_PAYMENT_METHOD_ID = 12 + # The LDAP host that is used to verify login attempts in the LDAP authentication module LDAP_HOST = 'hexia.ia.utwente.nl' @@ -450,9 +455,10 @@ # API documentation strings are formatted with markdown MODERNRPC_DOC_FORMAT = 'markdown' -# Settings regarding direct debits +# Settings regarding personal tab PERSONAL_TAB_MAXIMUM_ACTIVITY_PRICE = Decimal('30.00') # Maximum price of an activity PERSONAL_TAB_COMMITTEE_CAN_AUTHORIZE = True # Allow the committee to enroll people that pay with their authorization +PERSONAL_TAB_MINIMUM_BALANCE_REMINDER = Decimal('5.00') # Minimum balance above which people without authorization get monthly reminders to pay their open balance # Base path to the website. To be used when the django 'build_absolute_url()' does not work correctly. ABSOLUTE_PATH_TO_SITE = "https://staging.ia.utwente.nl" diff --git a/amelie/style/static/css/compiled.css b/amelie/style/static/css/compiled.css index c94d4cbc..e20ca8d3 100644 --- a/amelie/style/static/css/compiled.css +++ b/amelie/style/static/css/compiled.css @@ -10310,6 +10310,9 @@ fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus { background-repeat: no-repeat; line-height: 1.5; } +.icon.scaled-icon { + background-size: contain; +} .icon[type="button"], .icon[type="submit"], .icon.looks-like-a-button { @@ -10325,6 +10328,9 @@ fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus { background-repeat: no-repeat; line-height: 1.5; } +.news-pinned.scaled-icon { + background-size: contain; +} .news-pinned[type="button"], .news-pinned[type="submit"], .news-pinned.looks-like-a-button { @@ -10564,6 +10570,39 @@ li.courses-soon { .icon-enroll { background: transparent url("/static/img/icons/calendar_add.png") no-repeat; } +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-bold { + font-weight: bold; +} +.text-italics { + font-style: italic; +} +.text-underline { + text-decoration: underline; +} .hidden { display: none; } @@ -10710,6 +10749,9 @@ li.courses-soon { color: #FFFFFF; font-weight: bold; } +.ia th.secondary { + background: #2450A8; +} .ia .zebra thead tr th { color: #FFFFFF; background-color: #1d428a; @@ -10807,6 +10849,9 @@ li.courses-soon { color: #FFFFFF; font-weight: bold; } +.current th.secondary { + background: #099A2B; +} .current .zebra thead tr th { color: #FFFFFF; background-color: #077821; @@ -10904,6 +10949,9 @@ li.courses-soon { color: #FFFFFF; font-weight: bold; } +.edu th.secondary { + background: #DC332E; +} .edu .zebra thead tr th { color: #FFFFFF; background-color: #B8231F; @@ -11194,6 +11242,9 @@ a.looks-like-a-button.obscure { padding: 6px 6px 6px 28px !important; border: 2px solid; } +.ia_messages li.scaled-icon { + background-size: contain; +} .ia_messages li[type="button"], .ia_messages li[type="submit"], .ia_messages li.looks-like-a-button { @@ -11653,6 +11704,9 @@ p { margin: 10px 0; word-wrap: break-word; } +p.large-text { + font-size: 125%; +} p.serial { padding: 0 5px; display: inline-block; @@ -12298,6 +12352,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/email.png") no-repeat; } +.claudia-email.scaled-icon { + background-size: contain; +} .claudia-email[type="button"], .claudia-email[type="submit"], .claudia-email.looks-like-a-button { @@ -12312,6 +12369,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/email_go.png") no-repeat; } +.claudia-alias.scaled-icon { + background-size: contain; +} .claudia-alias[type="button"], .claudia-alias[type="submit"], .claudia-alias.looks-like-a-button { @@ -12326,6 +12386,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/vcard.png") no-repeat; } +.claudia-account.scaled-icon { + background-size: contain; +} .claudia-account[type="button"], .claudia-account[type="submit"], .claudia-account.looks-like-a-button { @@ -12340,6 +12403,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/exclamation.png") no-repeat; } +.claudia-error.scaled-icon { + background-size: contain; +} .claudia-error[type="button"], .claudia-error[type="submit"], .claudia-error.looks-like-a-button { @@ -12355,6 +12421,9 @@ table.totals tr:nth-last-child(2) td { background: transparent url("/static/img/icons/status_offline.png") no-repeat; color: gray !important; } +.claudia-nomapping.scaled-icon { + background-size: contain; +} .claudia-nomapping[type="button"], .claudia-nomapping[type="submit"], .claudia-nomapping.looks-like-a-button { @@ -12369,6 +12438,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/user.png") no-repeat; } +.claudia-AmeliePerson.scaled-icon { + background-size: contain; +} .claudia-AmeliePerson[type="button"], .claudia-AmeliePerson[type="submit"], .claudia-AmeliePerson.looks-like-a-button { @@ -12383,6 +12455,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/group.png") no-repeat; } +.claudia-AmelieGroup.scaled-icon { + background-size: contain; +} .claudia-AmelieGroup[type="button"], .claudia-AmelieGroup[type="submit"], .claudia-AmelieGroup.looks-like-a-button { @@ -12397,6 +12472,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/group_link.png") no-repeat; } +.claudia-AmelieDoGroup.scaled-icon { + background-size: contain; +} .claudia-AmelieDoGroup[type="button"], .claudia-AmelieDoGroup[type="submit"], .claudia-AmelieDoGroup.looks-like-a-button { @@ -12411,6 +12489,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/group_add.png") no-repeat; } +.claudia-ExtraGroup.scaled-icon { + background-size: contain; +} .claudia-ExtraGroup[type="button"], .claudia-ExtraGroup[type="submit"], .claudia-ExtraGroup.looks-like-a-button { @@ -12425,6 +12506,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/user_add.png") no-repeat; } +.claudia-ExtraPerson.scaled-icon { + background-size: contain; +} .claudia-ExtraPerson[type="button"], .claudia-ExtraPerson[type="submit"], .claudia-ExtraPerson.looks-like-a-button { @@ -12439,6 +12523,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/folder_user.png") no-repeat; } +.claudia-SharedDrive.scaled-icon { + background-size: contain; +} .claudia-SharedDrive[type="button"], .claudia-SharedDrive[type="submit"], .claudia-SharedDrive.looks-like-a-button { @@ -12453,6 +12540,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/email_go.png") no-repeat; } +.claudia-AliasGroup.scaled-icon { + background-size: contain; +} .claudia-AliasGroup[type="button"], .claudia-AliasGroup[type="submit"], .claudia-AliasGroup.looks-like-a-button { @@ -12467,6 +12557,9 @@ table.totals tr:nth-last-child(2) td { line-height: 1.5; background: transparent url("/static/img/icons/email.png") no-repeat; } +.claudia-Contact.scaled-icon { + background-size: contain; +} .claudia-Contact[type="button"], .claudia-Contact[type="submit"], .claudia-Contact.looks-like-a-button { diff --git a/amelie/style/static/less/htmltags/content.less b/amelie/style/static/less/htmltags/content.less index 0dbbe688..46b942e0 100644 --- a/amelie/style/static/less/htmltags/content.less +++ b/amelie/style/static/less/htmltags/content.less @@ -92,6 +92,10 @@ p { margin: 10px 0; word-wrap: break-word; + &.large-text { + font-size: 125%; + } + &.serial { padding: 0 5px; display: inline-block; diff --git a/amelie/style/static/less/util/icons.less b/amelie/style/static/less/util/icons.less index 6ff2ce14..68bc2ce8 100644 --- a/amelie/style/static/less/util/icons.less +++ b/amelie/style/static/less/util/icons.less @@ -11,6 +11,10 @@ background-repeat: no-repeat; line-height: 1.5; + &.scaled-icon { + background-size: contain; + } + &[type="button"], &[type="submit"], &.looks-like-a-button { padding-left: 26px; background-position-x: 4px; diff --git a/amelie/style/static/less/util/mixins.less b/amelie/style/static/less/util/mixins.less index 591c342f..818ac825 100644 --- a/amelie/style/static/less/util/mixins.less +++ b/amelie/style/static/less/util/mixins.less @@ -108,6 +108,10 @@ font-weight: bold; } + th.secondary { + background: @color_light; + } + .zebra { thead tr th { color: @white; diff --git a/amelie/style/static/less/util/text_styles.less b/amelie/style/static/less/util/text_styles.less new file mode 100644 index 00000000..cebeef9b --- /dev/null +++ b/amelie/style/static/less/util/text_styles.less @@ -0,0 +1,17 @@ + +// Alignment +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-justify { text-align: justify; } +.text-nowrap { white-space: nowrap; } + +// Transformation +.text-lowercase { text-transform: lowercase; } +.text-uppercase { text-transform: uppercase; } +.text-capitalize { text-transform: capitalize; } + +// Font styles +.text-bold { font-weight: bold; } +.text-italics { font-style: italic; } +.text-underline { text-decoration: underline; } diff --git a/amelie/style/static/less/utils.less b/amelie/style/static/less/utils.less index cedf1fc3..5c047464 100644 --- a/amelie/style/static/less/utils.less +++ b/amelie/style/static/less/utils.less @@ -3,6 +3,7 @@ @import "./util/datepicker"; @import "./util/timepicker"; @import "./util/icons"; +@import "./util/text_styles"; .hidden { display: none; diff --git a/amelie/test_translation.py b/amelie/test_translation.py index ba67205d..bd57ea4b 100644 --- a/amelie/test_translation.py +++ b/amelie/test_translation.py @@ -20,6 +20,7 @@ REGEX_MSGID = re.compile(r'(?P(#.*\n)+)msgid +(?P(".*"\n)+)') REGEX_IGNORED = [ re.compile(r'{% +blocktrans.*?%}.+?{% +endblocktrans +%}'), + re.compile(r'{% +blocktranslate.*?%}.+?{% +endblocktranslate +%}'), re.compile(r'{% +comment.*?%}.+?{% +endcomment +%}'), re.compile(r''), re.compile(r''), re.compile(r''), re.compile(r'{[{%#].+?[}%#]}'), re.compile(r']*?type="(?:input|submit|text)"value=(?:\'(?:\'[^>]*?>)?|"(?:"[^>]*?>)?)'), @@ -39,7 +40,8 @@ "\\bEUR\\b", "\\b-\\b", "\\bI\\.C\\.T\\.S\\.V\\.\\b", "\\bInter-\\b", "\\bActief\\b", "\\+53 489 3756", "contact@inter-actief\\.net", "https://ictsv\\.nl/wa", "https://ictsv\\.nl/discord", "@inter\\.actief", - "https://www\\.linkedin\\.com/company/i\\.c\\.t\\.s\\.v\\.-inter-actief/", "\\*", "#" + "https://www\\.linkedin\\.com/company/i\\.c\\.t\\.s\\.v\\.-inter-actief/", "\\*", "#", + "€", " ", "€ " ] diff --git a/amelie/tools/locale.py b/amelie/tools/locale.py new file mode 100644 index 00000000..0943f1c4 --- /dev/null +++ b/amelie/tools/locale.py @@ -0,0 +1,21 @@ +import locale +import threading +from contextlib import contextmanager + + +LOCALE_LOCK = threading.Lock() + + +@contextmanager +def setlocale(name): + """ + A Contextmanager to change the locale within the context to the given locale string. + This allows tools like strptime to parse time strings in other locales than the system locale. + e.g. "with setlocale('en_US'): ..." + """ + with LOCALE_LOCK: + saved = locale.setlocale(locale.LC_ALL) + try: + yield locale.setlocale(locale.LC_ALL, name) + finally: + locale.setlocale(locale.LC_ALL, saved) diff --git a/amelie/tools/mail.py b/amelie/tools/mail.py index 623f4d67..9afb42c9 100644 --- a/amelie/tools/mail.py +++ b/amelie/tools/mail.py @@ -50,6 +50,7 @@ def __init__(self, recipient, context=None, headers=None, ccs=None, bccs=None, a super(PersonRecipient, self).__init__(tos, context, headers, ccs, bccs, language, attachments) + class ActiveMemberRecipient(Recipient): def __init__(self, recipient, context=None, headers=None, ccs=None, bccs=None, attachments=None): """ diff --git a/amelie/tools/management/commands/www_generate_dummydata.py b/amelie/tools/management/commands/www_generate_dummydata.py index d9057af4..a087cad1 100644 --- a/amelie/tools/management/commands/www_generate_dummydata.py +++ b/amelie/tools/management/commands/www_generate_dummydata.py @@ -1,6 +1,7 @@ import datetime import getpass import io +import math import os import random import uuid @@ -274,6 +275,10 @@ def handle(self, *args, **options): # The Personal Tab default settings require that at least 3 discount periods exist baker.make('personal_tab.DiscountPeriod', _quantity=3) + # Create a few PaymentMethods for the Personal Tab + self.stdout.write("- 4 payment methods...") + payment_methods = baker.make('personal_tab.PaymentMethod', _quantity=4) + # The SEPA authorizations require that an AuthorizationType exists for Contribution and for other stuff if not AuthorizationType.objects.filter(active=True, contribution=True).exists(): AuthorizationType.objects.create(name_nl="Contributie", name_en="Contribution", @@ -309,37 +314,40 @@ def handle(self, *args, **options): # 10 Preferences self.stdout.write("- 10 preferences...") baker.make('members.Preference', _quantity=10) - # 2 PaymentTypes - self.stdout.write("- 2 payment types...") - baker.make('members.PaymentType', _quantity=2) # 80 Persons with Memberships and Students and StudyPeriods self.stdout.write("- 80 persons with memberships, students and study periods...") students = baker.make(PERSON_MODEL_STRING, user=None, _quantity=80) for student in students: - baker.make('members.Membership', + ms = baker.make('members.Membership', member=student, year=current_association_year(), ended=None) + for m in ms: + m.create_contribution_transaction() s = baker.make('members.Student', person=student) baker.make('members.StudyPeriod', student=s, begin=gen_date_before_today) # 10 Persons with expired memberships (old members) and Students and StudyPeriods self.stdout.write("- 10 persons with expired memberships (old members), students and study periods...") students = baker.make(PERSON_MODEL_STRING, user=None, _quantity=10) for student in students: - baker.make('members.Membership', + ms = baker.make('members.Membership', member=student, year=current_association_year()-1, ended=None) + for m in ms: + m.create_contribution_transaction() s = baker.make('members.Student', person=student) baker.make('members.StudyPeriod', student=s, begin=gen_date_before_today) # 20 Persons with Memberships and Employees self.stdout.write("- 20 persons with memberships and employees...") employees = baker.make(PERSON_MODEL_STRING, user=None, _quantity=20) for employee in employees: - baker.make('members.Membership', + ms = baker.make('members.Membership', member=employee, year=current_association_year(), ended=None) + for m in ms: + m.create_contribution_transaction() baker.make('members.Employee', person=employee, end=None) # 50 Payments (half of students and half of employees) self.stdout.write("- 50 payments for half of the students and half of the employees...") @@ -348,9 +356,9 @@ def handle(self, *args, **options): random.shuffle(employees) half_employees = employees[:10] for person in half_students + half_employees: - baker.make('members.Payment', - membership=person.membership_set.first(), - amount=person.membership_set.first().type.price) + # Mark membership as paid + m: Membership = person.membership_set.first() + m.mark_as_paid(payment_method=random.choice(payment_methods)) # 2 CommitteeCategories self.stdout.write("- 2 committee categories...") baker.make('members.CommitteeCategory', _quantity=2) @@ -379,7 +387,7 @@ def handle(self, *args, **options): _quantity=20) # 20 activities with unlimited enrollments between three weeks ago and three weeks from now, # with between 0 and 10 enrollments - self.stdout.write("- 20 activities with unlimited enrollments and 0 to 10 enrollments, " + self.stdout.write("- 20 activities with unlimited enrollments and 0 to 10 enrollments (of which about half are paid), " "between 3 weeks ago and 3 weeks from now...") as_unlimited_enroll = baker.make(ACTIVITY_MODEL_STRING, begin=gen_datetime_between( @@ -403,10 +411,16 @@ def handle(self, *args, **options): for activity in as_unlimited_enroll: num_enrollments = random.randint(0, 10) if num_enrollments != 0: - baker.make('calendar.Participation', event=activity, _quantity=num_enrollments) + participations = baker.make('calendar.Participation', event=activity, _quantity=num_enrollments) + # Mark about half of the participations as paid + random.shuffle(participations) + half_participations = participations[:math.ceil(num_enrollments/2)] + for participation in half_participations: + # Mark participation as paid + participation.mark_as_paid(payment_method=random.choice(payment_methods)) # 20 activities with limited enrollments between three weeks ago and three weeks from now, - # with between 0 and 10 enrollments - self.stdout.write("- 20 activities with limited enrollments and 0 to 10 enrollments, " + # with between 0 and 10 enrollments (of which about half are paid) + self.stdout.write("- 20 activities with limited enrollments and 0 to 10 enrollments (of which about half are paid), " "between 3 weeks ago and 3 weeks from now...") as_limited_enroll = baker.make(ACTIVITY_MODEL_STRING, begin=gen_datetime_between( @@ -431,8 +445,16 @@ def handle(self, *args, **options): for activity in as_limited_enroll: num_enrollments = random.randint(0, 10) if num_enrollments != 0: - baker.make('calendar.Participation', event=activity, _quantity=num_enrollments) + participations = baker.make('calendar.Participation', event=activity, _quantity=num_enrollments) + # Mark about half of the participations as paid + random.shuffle(participations) + half_participations = participations[:math.ceil(num_enrollments/2)] + for participation in half_participations: + # Mark participation as paid + participation.mark_as_paid(payment_method=random.choice(payment_methods)) + # TODO: Enrollment options + # Add between 1 and 30 pictures to the 20 limited enrollment activities self.stdout.write("- Between 1 and 30 pictures for the 20 limited enrollment activities...") attachment_recipe = Recipe('files.Attachment', _create_files=True, file=gen_image_by_dimensions(800, 600), diff --git a/amelie/tools/mixins.py b/amelie/tools/mixins.py index f0115034..a2ed36c2 100644 --- a/amelie/tools/mixins.py +++ b/amelie/tools/mixins.py @@ -89,6 +89,12 @@ async def dispatch(self, request, *args, **kwargs): return await super(PassesTestAsyncMixin, self).dispatch(request, *args, **kwargs) +class RequireAjaxMixin(PassesTestMixin): + reason = _l('AJAX request required') + + def test_requirement(self, request): + return request.headers.get('x-requested-with') == 'XMLHttpRequest' + class RequirePersonMixin(PassesTestMixin): needs_login = True reason = _l('Access for (former) members only.') diff --git a/amelie/tools/templatetags/class_helpers.py b/amelie/tools/templatetags/class_helpers.py new file mode 100644 index 00000000..4ee8e33d --- /dev/null +++ b/amelie/tools/templatetags/class_helpers.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.filter +def get_class(value): + return value.__class__.__name__ diff --git a/amelie/tools/tests.py b/amelie/tools/tests.py index 5b01ea1d..248c5390 100644 --- a/amelie/tools/tests.py +++ b/amelie/tools/tests.py @@ -124,6 +124,10 @@ def load_basic_data(self): username1 = 'testuser' username2 = 'testuser2' + # Membership type + membership_type = MembershipType(name_nl='Test membership', price=13.37) + membership_type.save() + data['password1'] = 'testuser13475' data['password2'] = 'testuser23475' @@ -135,6 +139,9 @@ def load_basic_data(self): account_name=data['user1'].username) data['person1'].save() + data['membership1'] = Membership(member=data['person1'], type=membership_type, year=current_association_year()) + data['membership1'].save() + data['user2'] = User(username=username2, first_name='Test2', last_name='Client', email='test2@example.com') data['user2'].set_password(data['password2']) data['user2'].save() @@ -143,6 +150,9 @@ def load_basic_data(self): account_name=data['user2'].username) data['person2'].save() + data['membership2'] = Membership(member=data['person2'], type=membership_type, year=current_association_year()) + data['membership2'].save() + # Committee data['committee1'] = Committee(name='Committee 1', abbreviation='Com1') data['committee1'].save() diff --git a/amelie/urls.py b/amelie/urls.py index 5079659f..d6730471 100644 --- a/amelie/urls.py +++ b/amelie/urls.py @@ -55,7 +55,6 @@ path('videos/', include('amelie.videos.urls')), path('data_export/', include('amelie.data_export.urls')), path('publications/', include('amelie.publications.urls')), - path('participation/', include('amelie.calendar.participation_urls')), # API path('api/', include('amelie.api.urls')), diff --git a/amelie/views.py b/amelie/views.py index 1b8686bd..8d759033 100644 --- a/amelie/views.py +++ b/amelie/views.py @@ -32,8 +32,7 @@ from amelie.education.models import Complaint, EducationEvent from amelie.statistics.decorators import track_hits from amelie.tools.auth import get_user_info, unlink_totp, unlink_acount, unlink_passkey, register_totp, register_passkey -from amelie.tools.mixins import RequirePersonMixin, RequireSuperuserMixin, RequireBoardMixin, RequireActiveMemberMixin, \ - RequireSuperuserAsyncMixin +from amelie.tools.mixins import RequirePersonMixin, RequireSuperuserMixin, RequireBoardMixin, RequireSuperuserAsyncMixin from amelie.tools.buildinfo import get_build_info from amelie.tools.models import Profile from amelie.videos.models import BaseVideo diff --git a/pyproject.toml b/pyproject.toml index 872f14ff..9261aade 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "django-localflavor>=5.0,<5.1", "django-extensions>=3.2.1,<4.2", "django-formtools>=2.4,<2.8", + "django-betterforms>=3.0.0,<3.1", "django-auditlog>=3.4.1,<3.5", "django-oauth-toolkit>=1.7.1,<3.3", "django-cors-headers>=3.13.0,<4.10", diff --git a/templates/profile_overview.html b/templates/profile_overview.html index 875ac4b4..c3050271 100644 --- a/templates/profile_overview.html +++ b/templates/profile_overview.html @@ -58,11 +58,17 @@

    {% trans 'Membership' %}

    {% endblocktrans %}

    - {% if membership.is_paid %} + {% if membership.is_free %} +

    {% blocktrans %} + Your membership type does not require payment. + {% endblocktrans %}

    + {% elif membership.is_paid %} {% else %}

    From 06fce96d4aad6e0cf181b6bf4f9fe20a1fb1e1dd Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Sat, 8 Aug 2026 10:52:39 +0200 Subject: [PATCH 14/39] General cleanup, create ContributionTransactions on the proper date, add Django Admin views for new models, fix incorrect auto-calculated settlement amount if it is not passed to the create method, set transaction added_by in settlement transactions --- amelie/activities/ajax_views.py | 3 - amelie/activities/graphql.py | 3 +- amelie/activities/mail.py | 3 +- amelie/activities/models.py | 13 -- amelie/api/person.py | 1 - ...articipation_cash_payment_made_and_more.py | 2 +- .../management/commands/yeartransfer.py | 6 +- .../0024_delete_payment_delete_paymenttype.py | 2 +- amelie/members/models.py | 6 +- amelie/personal_tab/admin.py | 149 +++++++++++++++--- amelie/personal_tab/debt_collection.py | 3 +- amelie/personal_tab/managers.py | 1 - .../0014_personaltabsettlement_and_more.py | 2 +- amelie/personal_tab/models.py | 11 +- amelie/personal_tab/statistics.py | 1 - amelie/personal_tab/views.py | 6 +- .../commands/www_generate_dummydata.py | 8 +- amelie/tools/mixins.py | 2 +- amelie/tools/views.py | 1 - amelie/urls.py | 1 - 20 files changed, 155 insertions(+), 69 deletions(-) diff --git a/amelie/activities/ajax_views.py b/amelie/activities/ajax_views.py index 33b76c6e..753165d7 100644 --- a/amelie/activities/ajax_views.py +++ b/amelie/activities/ajax_views.py @@ -3,15 +3,12 @@ from django.conf import settings from django.http import HttpResponseRedirect from django.urls import reverse -from django.utils import translation -from django.utils.translation import gettext as _ from django.views.generic import DetailView, FormView from amelie.activities.forms import ActivityManualPaymentForm from amelie.activities.views import ActivityDetailView from amelie.calendar.models import Participation from amelie.members.models import Person -from amelie.personal_tab.models import ManualPaymentSettlement from amelie.tools.mixins import RequireAjaxMixin, RequireCommitteeMixin diff --git a/amelie/activities/graphql.py b/amelie/activities/graphql.py index 4d18e7a2..6c0f47c3 100644 --- a/amelie/activities/graphql.py +++ b/amelie/activities/graphql.py @@ -1,8 +1,7 @@ import graphene from django_filters import FilterSet, BooleanFilter from django.utils.translation import gettext_lazy as _ -from django.db.models import Q, Count -from docutils.nodes import description +from django.db.models import Count from graphene_django import DjangoObjectType from amelie.activities.models import Activity, ActivityLabel diff --git a/amelie/activities/mail.py b/amelie/activities/mail.py index af678b0a..53d7bf27 100644 --- a/amelie/activities/mail.py +++ b/amelie/activities/mail.py @@ -1,9 +1,8 @@ from django.utils import translation -from amelie.calendar.models import Participation from amelie.iamailer import MailTask, Recipient from amelie.tools.const import TaskPriority -from amelie.members.models import Preference, Person +from amelie.members.models import Preference from amelie.tools.calendar import ical_calendar from amelie.tools.mail import PersonRecipient from django.utils.translation import gettext_lazy as _l diff --git a/amelie/activities/models.py b/amelie/activities/models.py index 47532b3a..3f70aec2 100644 --- a/amelie/activities/models.py +++ b/amelie/activities/models.py @@ -386,19 +386,6 @@ def uncancel_activity(self): self.cancelled = False self.save() - def enroll_person(self, person: Person): - # TODO Implement - pass - - def unenroll_person(self, person: Person): - # TODO Implement - pass - - def edit_enrollment(self, participation: Participation): - # TODO Implement - pass - - class Enrollmentoption(models.Model): activity = models.ForeignKey(Activity, on_delete=models.CASCADE) diff --git a/amelie/api/person.py b/amelie/api/person.py index 961cebb4..2fe0c0f0 100644 --- a/amelie/api/person.py +++ b/amelie/api/person.py @@ -8,7 +8,6 @@ from amelie.api.api import api_server from amelie.api.decorators import auth_required -from amelie.members.models import Person @api_server.register_procedure(name='getUserId', auth=auth_required(), context_target='ctx') diff --git a/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py b/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py index 9109cf3b..e312048e 100644 --- a/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py +++ b/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py @@ -105,7 +105,7 @@ def create_settlement_for_transactions(ManualPaymentSettlement, SettlementManual if total_transactions_price != 0: settlement_transaction = SettlementManualPaymentTransaction( date=payment_datetime, price=-total_transactions_price, person=person, - description=description[:200], settlement=settlement + description=description[:200], settlement=settlement, added_by=created_by ) settlement_transaction.save() # Register this manual payment as the payment transaction for the settlement diff --git a/amelie/members/management/commands/yeartransfer.py b/amelie/members/management/commands/yeartransfer.py index 5d9f133f..afc3be5b 100644 --- a/amelie/members/management/commands/yeartransfer.py +++ b/amelie/members/management/commands/yeartransfer.py @@ -1,6 +1,7 @@ import datetime from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone from django.utils.encoding import smart_str from amelie.tools.const import TaskPriority @@ -188,8 +189,9 @@ def handle(self, *args, **options): member=membership.member, type=next_type, year=new_year ) if created: - # Create a ContributionTransaction for the membership, if necessary, so it will be paid for. - m.create_contribution_transaction() + # Create a ContributionTransaction on 07-01 for the membership, if necessary, so it will be paid for. + ct_date = timezone.make_aware(datetime.datetime(year=membership.year, month=7, day=1)) + m.create_contribution_transaction(date=ct_date) else: errorfile.write(smart_str(f"{membership.member} ({membership.type}) was already enrolled for the new year\n")) diff --git a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py index ec9bc2e7..a1e19bd2 100644 --- a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py +++ b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py @@ -55,7 +55,7 @@ def create_settlement_for_transactions(ManualPaymentSettlement, SettlementManual if total_transactions_price != 0: settlement_transaction = SettlementManualPaymentTransaction( date=payment_datetime, price=-total_transactions_price, person=person, - description=description[:200], settlement=settlement + description=description[:200], settlement=settlement, added_by=created_by ) settlement_transaction.save() # Register this manual payment as the payment transaction for the settlement diff --git a/amelie/members/models.py b/amelie/members/models.py index f874a0e2..94256c8a 100644 --- a/amelie/members/models.py +++ b/amelie/members/models.py @@ -1048,7 +1048,11 @@ def create_contribution_transaction(self, date: Optional[datetime.datetime] = No """ from amelie.personal_tab.models import ContributionTransaction # Avoid circular import if date is None: - date = timezone.now() + # Today if the membership is for a current or past association year, 07-01-year if it is in the future. + if self.year <= current_association_year(): + date = timezone.now() + else: + date = timezone.make_aware(datetime.datetime(year=self.year, month=7, day=1)) if not self.contributiontransaction_set.exists() and self.type.price != 0: with translation.override(self.member.preferred_language): ct = ContributionTransaction( diff --git a/amelie/personal_tab/admin.py b/amelie/personal_tab/admin.py index 2d7f0cb1..5169ad41 100644 --- a/amelie/personal_tab/admin.py +++ b/amelie/personal_tab/admin.py @@ -1,3 +1,5 @@ +from typing import Optional + from django.contrib import admin from amelie.personal_tab.models import Declaration, DiscountPeriod, Discount, DiscountCredit, Transaction, \ @@ -5,7 +7,7 @@ Article, Category, RFIDCard, AuthorizationType, Authorization, Amendment, DebtCollectionAssignment, \ DebtCollectionBatch, \ DebtCollectionInstruction, Reversal, DebtCollectionTransaction, ReversalTransaction, LedgerAccount, PrintLogEntry, \ - PaymentMethod + PaymentMethod, ManualPaymentSettlement, SettlementExtraBalanceTransaction, SettlementManualPaymentTransaction class RFIDAdmin(admin.ModelAdmin): @@ -27,13 +29,13 @@ class CookieCornerTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[CookieCornerTransaction] = None): if obj and obj.settlement: return False else: return super(CookieCornerTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[CookieCornerTransaction] = None): if obj and obj.settlement: return False else: @@ -55,13 +57,13 @@ class TransactionAdmin(admin.ModelAdmin): def has_add_permission(self, request): return False - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[Transaction] = None): if obj and obj.settlement: return False else: return super(TransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[Transaction] = None): if obj and obj.settlement: return False else: @@ -80,13 +82,13 @@ class CustomTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[CustomTransaction] = None): if obj and obj.settlement: return False else: return super(CustomTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[CustomTransaction] = None): if obj and obj.settlement: return False else: @@ -105,13 +107,13 @@ class ActivityTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[ActivityTransaction] = None): if obj and obj.settlement: return False else: return super(ActivityTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[ActivityTransaction] = None): if obj and obj.settlement: return False else: @@ -130,13 +132,13 @@ class AlexiaTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[AlexiaTransaction] = None): if obj and obj.settlement: return False else: return super(AlexiaTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[AlexiaTransaction] = None): if obj and obj.settlement: return False else: @@ -155,13 +157,13 @@ class ContributionTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[ContributionTransaction] = None): if obj and obj.settlement: return False else: return super(ContributionTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[ContributionTransaction] = None): if obj and obj.settlement: return False else: @@ -180,13 +182,13 @@ class DebtCollectionTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[DebtCollectionTransaction] = None): if obj and obj.settlement: return False else: return super(DebtCollectionTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[DebtCollectionTransaction] = None): if obj and obj.settlement: return False else: @@ -205,19 +207,57 @@ class ReversalTransactionAdmin(admin.ModelAdmin): # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored actions = None - def has_change_permission(self, request, obj=None): + def has_change_permission(self, request, obj: Optional[ReversalTransaction] = None): if obj and obj.settlement: return False else: return super(ReversalTransactionAdmin, self).has_change_permission(request, obj) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj: Optional[ReversalTransaction] = None): if obj and obj.settlement: return False else: return super(ReversalTransactionAdmin, self).has_delete_permission(request, obj) +class SettlementExtraBalanceTransactionAdmin(admin.ModelAdmin): + list_display = ('id', 'manual_settlement', 'description', 'price', 'person', 'discount', 'date', 'added_on') + list_filter = ('date', 'added_on') + date_hierarchy = 'date' + search_fields = ['description', 'person__slug'] + raw_id_fields = ('person', 'discount', 'settlement') + readonly_fields = ('added_on', 'added_by') + ordering = ['-added_on'] + + # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored + actions = None + + def has_change_permission(self, request, obj: Optional[SettlementExtraBalanceTransaction] = None): + return False + + def has_delete_permission(self, request, obj: Optional[SettlementExtraBalanceTransaction] = None): + return False # Deletion must go via ManualPaymentSettlement deletion + + +class SettlementManualPaymentTransactionAdmin(admin.ModelAdmin): + list_display = ('id', 'manual_settlement', 'description', 'price', 'person', 'discount', 'date', 'added_on') + list_filter = ('date', 'added_on') + date_hierarchy = 'date' + search_fields = ['description', 'person__slug'] + raw_id_fields = ('person', 'discount', 'settlement') + readonly_fields = ('added_on', 'added_by') + ordering = ['-added_on'] + + # Jelte 2015-04-01: Disable mass deletion because has_delete_permission seems to be ignored + actions = None + + def has_change_permission(self, request, obj: Optional[SettlementManualPaymentTransaction] = None): + return False + + def has_delete_permission(self, request, obj: Optional[SettlementManualPaymentTransaction] = None): + return False # Deletion must go via ManualPaymentSettlement deletion + + class CategoryAdmin(admin.ModelAdmin): list_display = ('id', 'name_nl', 'name_en', 'is_available', 'order') list_editable = ('order',) @@ -285,11 +325,34 @@ def has_add_permission(self, request): return False +class TransactionsInline(admin.TabularInline): + model = Transaction + fk_name = 'settlement' + extra = 0 + can_delete = False + show_change_link = True + + fields = ('id', 'date', 'price', 'description') + ordering = ['-added_on'] + + def has_add_permission(self, *args, **kwargs): + return False + + def has_change_permission(self, *args, **kwargs): + return False + + def has_delete_permission(self, *args, **kwargs): + return False + + class DebtCollectionInstructionAdmin(admin.ModelAdmin): list_display = ('id', 'batch', 'authorization', 'end_to_end_id', 'amount',) search_fields = ['authorization__person__first_name', 'authorization__person__last_name', 'authorization__iban', 'authorization__account_holder_name', 'description'] raw_id_fields = ('batch', 'authorization',) + inlines = [ + TransactionsInline + ] def has_add_permission(self, request): return False @@ -311,6 +374,34 @@ class PaymentMethodAdmin(admin.ModelAdmin): search_fields = ('name_nl', 'name_en', 'description_nl', 'description_en') +class ManualPaymentSettlementAdmin(admin.ModelAdmin): + list_display = ('id', 'payment_date', 'person', 'get_payment_method_display', 'amount', 'description') + list_filter = ('payment_date', 'payment_method') + search_fields = ['person__first_name', 'person__last_name', 'description'] + raw_id_fields = ('person', 'payment_method',) + readonly_fields = ('amount', 'person', 'payment_date', 'created_by', 'manual_payment_transaction', 'extra_balance_transaction') + inlines = [ + TransactionsInline + ] + + def get_payment_method_display(self, obj): + return obj.payment_method.name + get_payment_method_display.short_description = 'Payment method' + get_payment_method_display.admin_order_field = 'payment_method' + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj: Optional[ManualPaymentSettlement] = None): + if not obj or not obj.deletion_possible()[0]: + return False + else: + return super().has_change_permission(request, obj=obj) + + def has_delete_permission(self, request, obj: Optional[ManualPaymentSettlement] = None): + return False # Deletion only via website due to required deletion procedure + + class DiscountPeriodAdmin(admin.ModelAdmin): list_display = ('id', 'description_nl', 'description_en', 'begin', 'end', 'ledger_account_number', 'balance_account_number') @@ -358,6 +449,7 @@ def has_transaction(self, obj): has_transaction.boolean = True has_transaction.short_description = 'Paid' + class DeclarationAdmin(admin.ModelAdmin): list_display = ('id', 'person', 'committee', 'amount', 'submission_date') list_filter = ('submission_date', 'payment_method') @@ -369,27 +461,36 @@ class DeclarationAdmin(admin.ModelAdmin): admin.site.register(RFIDCard, RFIDAdmin) + admin.site.register(Transaction, TransactionAdmin) -admin.site.register(CookieCornerTransaction, CookieCornerTransactionAdmin) admin.site.register(ActivityTransaction, ActivityTransactionAdmin) admin.site.register(AlexiaTransaction, AlexiaTransactionAdmin) admin.site.register(ContributionTransaction, ContributionTransactionAdmin) +admin.site.register(CookieCornerTransaction, CookieCornerTransactionAdmin) +admin.site.register(CustomTransaction, CustomTransactionAdmin) admin.site.register(DebtCollectionTransaction, DebtCollectionTransactionAdmin) admin.site.register(ReversalTransaction, ReversalTransactionAdmin) -admin.site.register(CustomTransaction, CustomTransactionAdmin) +admin.site.register(SettlementExtraBalanceTransaction, SettlementExtraBalanceTransactionAdmin) +admin.site.register(SettlementManualPaymentTransaction, SettlementManualPaymentTransactionAdmin) + admin.site.register(Category, CategoryAdmin) admin.site.register(LedgerAccount, LedgerAccountAdmin) admin.site.register(Article, ArticleAdmin) + +admin.site.register(DiscountPeriod, DiscountPeriodAdmin) +admin.site.register(Discount, DiscountAdmin) +admin.site.register(DiscountCredit, DiscountCreditAdmin) +admin.site.register(PrintLogEntry, PrintLogAdmin) +admin.site.register(Declaration, DeclarationAdmin) + admin.site.register(AuthorizationType, AuthorizationTypeAdmin) admin.site.register(Authorization, AuthorizationAdmin) admin.site.register(Amendment, AmendmentAdmin) + admin.site.register(DebtCollectionAssignment, DebtCollectionAssignmentAdmin) admin.site.register(DebtCollectionBatch, DebtCollectionBatchAdmin) admin.site.register(DebtCollectionInstruction, DebtCollectionInstructionAdmin) admin.site.register(Reversal, ReversalAdmin) + admin.site.register(PaymentMethod, PaymentMethodAdmin) -admin.site.register(DiscountPeriod, DiscountPeriodAdmin) -admin.site.register(Discount, DiscountAdmin) -admin.site.register(DiscountCredit, DiscountCreditAdmin) -admin.site.register(PrintLogEntry, PrintLogAdmin) -admin.site.register(Declaration, DeclarationAdmin) +admin.site.register(ManualPaymentSettlement, ManualPaymentSettlementAdmin) diff --git a/amelie/personal_tab/debt_collection.py b/amelie/personal_tab/debt_collection.py index d43f80cd..c7be68fa 100644 --- a/amelie/personal_tab/debt_collection.py +++ b/amelie/personal_tab/debt_collection.py @@ -1,5 +1,4 @@ import datetime -from datetime import timezone as tz from django.db import transaction from django.db.models import Sum, Q @@ -7,7 +6,7 @@ from django.utils import timezone, translation from django.utils.translation import gettext as _ -from amelie.members.models import Person, Membership +from amelie.members.models import Person from amelie.personal_tab.models import Transaction, DebtCollectionInstruction, DebtCollectionBatch, \ DebtCollectionTransaction, ContributionTransaction, ReversalTransaction, Reversal, Amendment from amelie.tools.encodings import normalize_to_ascii diff --git a/amelie/personal_tab/managers.py b/amelie/personal_tab/managers.py index 339c6e0b..95454020 100644 --- a/amelie/personal_tab/managers.py +++ b/amelie/personal_tab/managers.py @@ -4,7 +4,6 @@ from django.db.models import Q from django.db.models.aggregates import Sum from django.utils import timezone -from datetime import timezone as tz from amelie.members.models import Person diff --git a/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py b/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py index 2cd7cfce..2728e9e5 100644 --- a/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py +++ b/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py @@ -45,7 +45,7 @@ def create_settlement_for_transactions(ManualPaymentSettlement, SettlementManual if total_transactions_price != 0: settlement_transaction = SettlementManualPaymentTransaction( date=payment_datetime, price=-total_transactions_price, person=person, - description=description[:200], settlement=settlement + description=description[:200], settlement=settlement, added_by=created_by ) settlement_transaction.save() # Register this manual payment as the payment transaction for the settlement diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index baba3e94..be314e69 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -1046,10 +1046,10 @@ class ManualPaymentSettlement(PersonalTabSettlement): created_by = models.ForeignKey(Person, verbose_name=_l('Created by'), blank=True, null=True, related_name="+", on_delete=models.PROTECT) # '+' related_name makes sure no reverse relation is added to Person - """The ManualPaymentTransaction that was created in this settlement.""" + """The SettlementManualPaymentTransaction that was created in this settlement.""" manual_payment_transaction = models.OneToOneField(SettlementManualPaymentTransaction, on_delete=models.RESTRICT, related_name='manual_settlement', blank=True, null=True) - """The ManualPaymentTransaction that was created in this settlement.""" + """The SettlementExtraBalanceTransaction that was created in this settlement.""" extra_balance_transaction = models.OneToOneField(SettlementExtraBalanceTransaction, on_delete=models.RESTRICT, related_name='manual_settlement', blank=True, null=True) class Meta: @@ -1159,7 +1159,7 @@ def create_for_transactions( # Calculate the total settlement amount total_transaction_price = sum(t.price for t in transactions) or Decimal("0.00") if paid_amount is None: - paid_amount = -total_transaction_price + paid_amount = total_transaction_price leftover_balance_amount = total_transaction_price - paid_amount # Get the default descriptions if none were given @@ -1199,7 +1199,7 @@ def create_for_transactions( if total_transaction_price != 0: payment_transaction = SettlementManualPaymentTransaction( date=payment_datetime, price=-total_transaction_price, person=person, - description=payment_description[:200], settlement=settlement + description=payment_description[:200], settlement=settlement, added_by=created_by ) payment_transaction.save() settlement.manual_payment_transaction = payment_transaction @@ -1211,7 +1211,8 @@ def create_for_transactions( leftover_balance_transaction = SettlementExtraBalanceTransaction( date=payment_datetime, price=leftover_balance_amount, person=person, description=leftover_balance_description[:200], - settlement=None # Leaves it as unpaid so it can be used. + settlement=None, # Leaves it as unpaid so it can be used. + added_by=created_by ) leftover_balance_transaction.save() settlement.extra_balance_transaction = leftover_balance_transaction diff --git a/amelie/personal_tab/statistics.py b/amelie/personal_tab/statistics.py index c657faad..1a5448f9 100644 --- a/amelie/personal_tab/statistics.py +++ b/amelie/personal_tab/statistics.py @@ -1,6 +1,5 @@ from decimal import Decimal -from django.db import connection from django.db.models import Count, Sum from django.db.models.functions import TruncDay from django.utils.translation import gettext_lazy as _l diff --git a/amelie/personal_tab/views.py b/amelie/personal_tab/views.py index c2444aad..6facaa51 100644 --- a/amelie/personal_tab/views.py +++ b/amelie/personal_tab/views.py @@ -1,6 +1,5 @@ # coding=utf-8 import datetime -from collections import OrderedDict from datetime import timezone as tz import logging from decimal import Decimal @@ -41,8 +40,9 @@ DebtCollectionForm, ReversalForm, SearchAuthorizationForm, AmendmentForm, DebtCollectionBatchForm, \ AuthorizationSelectForm, \ StatisticsForm, DeclarationForm, ManualPaymentSettlementForm, ManualPaymentTransactionFormSet -from amelie.personal_tab.debt_collection import delete_amendment, delete_reversal, edit_amendment, edit_reversal, generate_contribution_instructions, filter_contribution_instructions, \ - save_contribution_instructions, generate_cookie_corner_instructions, filter_cookie_corner_instructions, save_cookie_corner_instructions, \ +from amelie.personal_tab.debt_collection import delete_amendment, delete_reversal, edit_amendment, edit_reversal, \ + generate_contribution_instructions, filter_contribution_instructions, save_contribution_instructions, \ + generate_cookie_corner_instructions, filter_cookie_corner_instructions, save_cookie_corner_instructions, \ process_reversal, process_amendment from amelie.personal_tab.models import Amendment, Category, Declaration, Transaction, CookieCornerTransaction, \ ActivityTransaction, \ diff --git a/amelie/tools/management/commands/www_generate_dummydata.py b/amelie/tools/management/commands/www_generate_dummydata.py index a087cad1..17315ae9 100644 --- a/amelie/tools/management/commands/www_generate_dummydata.py +++ b/amelie/tools/management/commands/www_generate_dummydata.py @@ -328,8 +328,8 @@ def handle(self, *args, **options): baker.make('members.StudyPeriod', student=s, begin=gen_date_before_today) # 10 Persons with expired memberships (old members) and Students and StudyPeriods self.stdout.write("- 10 persons with expired memberships (old members), students and study periods...") - students = baker.make(PERSON_MODEL_STRING, user=None, _quantity=10) - for student in students: + expired_students = baker.make(PERSON_MODEL_STRING, user=None, _quantity=10) + for student in expired_students: ms = baker.make('members.Membership', member=student, year=current_association_year()-1, @@ -353,9 +353,11 @@ def handle(self, *args, **options): self.stdout.write("- 50 payments for half of the students and half of the employees...") random.shuffle(students) half_students = students[:40] + random.shuffle(expired_students) + half_expired_students = expired_students[:10] random.shuffle(employees) half_employees = employees[:10] - for person in half_students + half_employees: + for person in half_students + half_expired_students + half_employees: # Mark membership as paid m: Membership = person.membership_set.first() m.mark_as_paid(payment_method=random.choice(payment_methods)) diff --git a/amelie/tools/mixins.py b/amelie/tools/mixins.py index a2ed36c2..35f01e43 100644 --- a/amelie/tools/mixins.py +++ b/amelie/tools/mixins.py @@ -19,7 +19,7 @@ from amelie.members.models import Committee, DogroupGeneration from amelie.tools.logic import current_association_year -from amelie.tools.http import get_client_ips, is_allowed_ip, HttpJSONResponse +from amelie.tools.http import is_allowed_ip, HttpJSONResponse class PassesTestMixin(object): diff --git a/amelie/tools/views.py b/amelie/tools/views.py index 5933bcf2..1ccd9de9 100644 --- a/amelie/tools/views.py +++ b/amelie/tools/views.py @@ -1,6 +1,5 @@ import json import logging -import re from django.conf import settings from django.contrib import messages diff --git a/amelie/urls.py b/amelie/urls.py index d6730471..b329c811 100644 --- a/amelie/urls.py +++ b/amelie/urls.py @@ -6,7 +6,6 @@ from django.contrib.auth.views import LogoutView from django.views.generic.base import RedirectView from django.views.static import serve -from health_check.views import HealthCheckView from amelie import views from amelie.activities.feeds import Activities From 14813d65ef7122ec1d99a2642e0ecdb685310843 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Sat, 8 Aug 2026 12:17:13 +0200 Subject: [PATCH 15/39] Small fixes and improvements to BadBIC updates after reversal create/update/delete --- amelie/members/ajax_views.py | 7 +-- amelie/members/forms.py | 8 +-- amelie/personal_tab/debt_collection.py | 78 +++++++------------------- amelie/personal_tab/models.py | 25 ++++++++- 4 files changed, 48 insertions(+), 70 deletions(-) diff --git a/amelie/members/ajax_views.py b/amelie/members/ajax_views.py index cb6a8a37..53911af9 100644 --- a/amelie/members/ajax_views.py +++ b/amelie/members/ajax_views.py @@ -250,13 +250,10 @@ def person_mandate_new(request, id): if form.is_valid(): # If the filled BIC is in the Bad BIC list, ask for a confirmation. if BadBIC.objects.filter(bic=form.cleaned_data['bic']).exists(): - if 'confirm' in request.POST: - pass - else: + if 'confirm' not in request.POST: return render(request, "person_confirm_mandate.html", locals()) - + mandate: Authorization = form.save(commit=False) - mandate = form.save(commit=False) mandate.person = obj mandate.save() if form.cleaned_data.get('send_signature_request'): diff --git a/amelie/members/forms.py b/amelie/members/forms.py index 95ba9f03..8667efc5 100644 --- a/amelie/members/forms.py +++ b/amelie/members/forms.py @@ -356,7 +356,7 @@ def clean(self): if not cleaned_data['iban']: raise forms.ValidationError(_l('An IBAN is required.')) - cleaned_data['iban'], cleaned_data['bic'] = clean_iban_and_bic(cleaned_data.get('iban'), cleaned_data.get('bic')) + cleaned_data['iban'], cleaned_data['bic'] = clean_iban_and_bic(cleaned_data.get('iban'), cleaned_data.get('bic'), ignore_bad_bic=True) return cleaned_data @@ -660,7 +660,7 @@ def clean_student_number(self): return self.cleaned_data["student_number"] -def clean_iban_and_bic(iban, bic, registration=False): +def clean_iban_and_bic(iban, bic, ignore_bad_bic=False): iban = str(iban).strip() bic = str(bic).strip() @@ -679,8 +679,8 @@ def clean_iban_and_bic(iban, bic, registration=False): else: raise forms.ValidationError(_l('BIC could not be generated, please enter it yourself.')) - # If the BIC is in the Bad BICs list and it is filled in via the RegistrationForm, do not accept it. - if registration and BadBIC.objects.filter(bic=bic): + # If the BIC is in the Bad BICs list, and it can't be ignored, do not accept it. + if not ignore_bad_bic and BadBIC.objects.filter(bic=bic).exists(): raise forms.ValidationError(_l('The bank associated with your BIC does not accept SEPA Direct Debits.')) if bic[4:6] != iban[:2]: diff --git a/amelie/personal_tab/debt_collection.py b/amelie/personal_tab/debt_collection.py index 871229a0..44f4f72a 100644 --- a/amelie/personal_tab/debt_collection.py +++ b/amelie/personal_tab/debt_collection.py @@ -76,7 +76,7 @@ def generate_contribution_instructions(years): """ memberships = Membership.objects.filter(Q(ended__gt=datetime.date.today()) | Q(ended__isnull=True), payment__isnull=True, type__price__gt=0, year__in=years) - + result = { 'ongoing_frst': [], 'frst': [], @@ -336,11 +336,12 @@ def process_reversal(reversal, actor): # If the reversal reason is DNOR, the BIC of the authorization must be added to the Bad BIC list if reversal.reason == Reversal.ReversalReasons.DNOR: bad_bic = BadBIC.objects.filter(bic=instruction.authorization.bic).first() - if not bad_bic: - bad_bic = BadBIC(bic=instruction.authorization.bic, first_reversal=reversal, last_reversal=reversal) + if bad_bic: + bad_bic.update_reversals() else: - bad_bic.last_reversal = reversal - bad_bic.save() + BadBIC.objects.create( + bic=instruction.authorization.bic, first_reversal=reversal, last_reversal=reversal + ) def edit_reversal(reversal, actor): @@ -357,36 +358,10 @@ def edit_reversal(reversal, actor): rt.added_by = actor rt.save() - # If the reversal reason is DNOR, the BIC of the authorization must be added to the Bad BIC list - if reversal.reason == Reversal.ReversalReasons.DNOR: - bad_bic = BadBIC.objects.filter(bic=instruction.authorization.bic).first() - if not bad_bic: - bad_bic = BadBIC(bic=instruction.authorization.bic, first_reversal=reversal, last_reversal=reversal) - else: - bad_bic.last_reversal = reversal - bad_bic.save() - - # Otherwise, check if there wasn't any BadBIC made because of the reversal before the edit - else: - bad_bic_first = BadBIC.objects.filter(first_reversal=reversal).first() - bad_bic_last = BadBIC.objects.filter(last_reversal=reversal).first() - - # If a BIC exists where this reversal was the first occurence - if bad_bic_first: - # If it was the first and last occurence, we can delete the BIC from the list - if bad_bic_first.last_reversal == reversal: - bad_bic_first.delete() - # Otherwise, we set the first occurence to the last occurence - else: - bad_bic_first.first_reversal = bad_bic_first.last_reversal - bad_bic_first.save() - - # Otherwise, if a BIC exists where this reversal was the last occurence - elif bad_bic_last: - # We set the last occurence to the first occurence - bad_bic_last.last_reversal = bad_bic_last.first_reversal - bad_bic_last.save() - + # Update any BadBic objects with this BIC or this reversal as their first/last reversal + bad_bics = list(BadBIC.objects.filter(Q(bic=instruction.authorization.bic) | Q(first_reversal=reversal) | Q(last_reversal=reversal))) + for bad_bic in bad_bics: + bad_bic.update_reversals() def delete_reversal(reversal): @@ -406,36 +381,21 @@ def delete_reversal(reversal): pm = Payment(membership=ct.membership, payment_type=PaymentType.objects.get(id=4), amount=ct.price, date=ct.date) pm.save() - + # Now we filter for the reversal ContributionTransaction that was created when the reversal was processed and delete it. cct = ContributionTransaction.objects.filter(membership=ct.membership, price=-ct.price) cct.delete() - - # If the reversal reason was DNOR, the BIC of the authorization must be removed from the Bad BIC list - if reversal.reason == Reversal.ReversalReasons.DNOR: - bad_bic_first = BadBIC.objects.filter(first_reversal=reversal).first() - bad_bic_last = BadBIC.objects.filter(last_reversal=reversal).first() - - # If a BIC exists where this reversal was the first occurence - if bad_bic_first: - # If it was the first and last occurence, we can delete the BIC from the list - if bad_bic_first.last_reversal == reversal: - bad_bic_first.delete() - # Otherwise, we set the first occurence to the last occurence - else: - bad_bic_first.first_reversal = bad_bic_first.last_reversal - bad_bic_first.save() - - # Otherwise, if a BIC exists where this reversal was the last occurence - elif bad_bic_last: - # We set the last occurence to the first occurence - bad_bic_last.last_reversal = bad_bic_last.first_reversal - bad_bic_last.save() - # Finally, we delete the Reversal object itself + reversal_reason = reversal.reason reversal.delete() + # If the reversal reason was DNOR, the BadBIC with this BIC should be re-checked if it exists + if reversal_reason == Reversal.ReversalReasons.DNOR: + bad_bic = BadBIC.objects.filter(bic=instruction.authorization.bic).first() + if bad_bic: + bad_bic.update_reversals() + def process_amendment(authorization, date, iban, bic, reason): """ @@ -479,7 +439,7 @@ def delete_amendment(amendment): Delete an unsent amendment to an authorization. Make sure you call this method only from within a database transaction! - + Make sure you call this method only for amendments that have not yet been sent to the bank! """ diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 83b39649..3b557a7f 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -959,12 +959,33 @@ class BadBIC(models.Model): bic = BICField(verbose_name=_l('BIC'), unique=True) date_added = models.DateTimeField(verbose_name=_l('Date added'), auto_now_add=True) - first_reversal = models.ForeignKey(Reversal, verbose_name=_l("First reversal"), related_name=_l('+'), null=True, on_delete=models.SET_NULL) - last_reversal = models.ForeignKey(Reversal, verbose_name=_l("Last reversal"), related_name=_l('+'), null=True, on_delete=models.SET_NULL) + first_reversal = models.ForeignKey(Reversal, verbose_name=_l("First reversal"), related_name='+', null=True, on_delete=models.SET_NULL) + last_reversal = models.ForeignKey(Reversal, verbose_name=_l("Last reversal"), related_name='+', null=True, on_delete=models.SET_NULL) def __str__(self): return self.bic + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.update_reversals() + + def update_reversals(self): + # Update the first and last reversal for this BadBIC + self.first_reversal = Reversal.objects.filter( + instruction__authorization__bic=self.bic, + reason=Reversal.ReversalReasons.DNOR + ).order_by('date').first() + self.last_reversal = Reversal.objects.filter( + instruction__authorization__bic=self.bic, + reason=Reversal.ReversalReasons.DNOR + ).order_by('date').last() + + # If both first and last are None (no reversals any more for this BIC), self-destruct. + if self.first_reversal is None and self.last_reversal is None: + self.delete() + else: + self.save() + class Meta: ordering = ['bic'] verbose_name = _l('known bad BIC') From 1bbcea8afeef04403e0c0b79686c7ecd6dd14aa3 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 10 Aug 2026 10:18:26 +0200 Subject: [PATCH 16/39] Fix conflicting migration --- .../0009_remove_participation_cash_payment_made_and_more.py | 2 +- .../migrations/0024_delete_payment_delete_paymenttype.py | 4 ++-- ...ent_and_more.py => 0015_personaltabsettlement_and_more.py} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename amelie/personal_tab/migrations/{0014_personaltabsettlement_and_more.py => 0015_personaltabsettlement_and_more.py} (100%) diff --git a/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py b/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py index e312048e..ca410a44 100644 --- a/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py +++ b/amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py @@ -200,7 +200,7 @@ class Migration(migrations.Migration): dependencies = [ ('calendar', '0008_alter_event_participants'), - ('personal_tab', '0014_personaltabsettlement_and_more'), + ('personal_tab', '0015_personaltabsettlement_and_more'), ('activities', '0013_activity_enrollment_private') ] diff --git a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py index a1e19bd2..341306c1 100644 --- a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py +++ b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py @@ -213,7 +213,7 @@ def forwards(apps, schema_editor): if hasattr(membership, 'payment') and membership.payment.payment_type != authorization_payment_type: unpaid_transactions = membership.contributiontransaction_set.filter(price__gt=0, settlement=None) if unpaid_transactions.exists(): - # Must exist due to being created in migration personal_tab.0014_personaltabsettlement_and_more + # Must exist due to being created in migration personal_tab.0015_personaltabsettlement_and_more payment_method = PaymentMethod.objects.get(name_en=membership.payment.payment_type.name) if membership.payment.date: transaction_date = datetime.datetime.combine( @@ -283,7 +283,7 @@ class Migration(migrations.Migration): dependencies = [ ('members', '0023_documenso_id_signed_document'), - ('personal_tab', '0014_personaltabsettlement_and_more'), + ('personal_tab', '0015_personaltabsettlement_and_more'), ] operations = [ diff --git a/amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py b/amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py similarity index 100% rename from amelie/personal_tab/migrations/0014_personaltabsettlement_and_more.py rename to amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py From a327a5d8fb4ce390eb7087bb3ab4b6965a8342dc Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 10 Aug 2026 11:05:58 +0200 Subject: [PATCH 17/39] Fix conflicting migration --- .../migrations/0015_personaltabsettlement_and_more.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py b/amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py index 2728e9e5..f24e263e 100644 --- a/amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py +++ b/amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py @@ -189,7 +189,7 @@ class Migration(migrations.Migration): dependencies = [ ('members', '0023_documenso_id_signed_document'), - ('personal_tab', '0013_authorization_documenso_id_and_more'), + ('personal_tab', '0014_pendingregistertoken'), ] operations = [ From b1ae09c94e5f6b6d32fde6021dd56f462119a916 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:05:29 +0000 Subject: [PATCH 18/39] build(deps): update model-bakery requirement Updates the requirements on [model-bakery](https://github.com/model-bakers/model_bakery) to permit the latest version. - [Release notes](https://github.com/model-bakers/model_bakery/releases) - [Changelog](https://github.com/model-bakers/model_bakery/blob/main/CHANGELOG.md) - [Commits](https://github.com/model-bakers/model_bakery/compare/1.23.3...1.24.0) --- updated-dependencies: - dependency-name: model-bakery dependency-version: 1.24.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28a9651a..177f96cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,7 @@ dependencies = [ "django-rosetta>=0.10.3,<0.11", "docutils>=0.19,<0.23", "pyinotify", - "model_bakery>=1.23.3,<1.24", + "model_bakery>=1.23.3,<1.25", "django-silk>=5.5.0,<5.6", # Single Sign On - OIDC Client From 3318f806ee905829d3d81f676c824fd22db0fd0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:05:34 +0000 Subject: [PATCH 19/39] build(deps): update pyopenssl requirement Updates the requirements on [pyopenssl](https://github.com/pyca/pyopenssl) to permit the latest version. - [Changelog](https://github.com/pyca/pyopenssl/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/pyopenssl/compare/26.0.0...26.4.0) --- updated-dependencies: - dependency-name: pyopenssl dependency-version: 26.4.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28a9651a..18fb7bcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ dependencies = [ # Google API Client "google-api-python-client>=2.192.0,<3", "google-auth>=2.49.1,<2.57", - "pyOpenSSL>=26.0.0,<26.4.0", + "pyOpenSSL>=26.0.0,<26.5.0", # Development "django-debug-toolbar>=3.7,<7.1", From c7dbdb96491c09db3557582936b1bed4e826209b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:05:43 +0000 Subject: [PATCH 20/39] build(deps): update python-gitlab requirement Updates the requirements on [python-gitlab](https://github.com/python-gitlab/python-gitlab) to permit the latest version. - [Release notes](https://github.com/python-gitlab/python-gitlab/releases) - [Changelog](https://github.com/python-gitlab/python-gitlab/blob/main/CHANGELOG.md) - [Commits](https://github.com/python-gitlab/python-gitlab/compare/v8.1.0...v8.5.0) --- updated-dependencies: - dependency-name: python-gitlab dependency-version: 8.5.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28a9651a..8d19df17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ dependencies = [ # Pillow requires libjpeg-dev zlib1g-dev (on Debian) "Pillow>=12.1.1,<12.4", - "python-gitlab>=8.1.0,<8.5", + "python-gitlab>=8.1.0,<8.6", "python-dateutil>=2.9.0,<2.10", # python-ldap requires libldap-dev libsasl2-dev (on Debian) to build From 4f08907321e1e71c2b61b00a5a4451e6b032998b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:05:53 +0000 Subject: [PATCH 21/39] build(deps): update sentry-sdk requirement Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/2.54.0...2.66.1) --- updated-dependencies: - dependency-name: sentry-sdk dependency-version: 2.66.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28a9651a..907d459f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,7 +114,7 @@ dependencies = [ "mozilla-django-oidc>=5.0.2,<5.1", # Sentry error logging - "sentry-sdk>=2.54.0,<2.65", + "sentry-sdk>=2.54.0,<2.67", # Health checks for Django "django-health-check[psutil,celery,rabbitmq,redis]>=4.0,<4.5", From fd805c8cfa408f13cbb710044b018b5d8d6cb8fe Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Wed, 12 Aug 2026 15:42:54 +0200 Subject: [PATCH 22/39] Fixes after review: - Only allow activity prices to be positive (including enrollments), to avoid incorrect behaviour w.r.t. transaction handling - Add activity name and print datetime to printed enrollment form - Show amount to be paid for manual enrollments on activity enrollment overview and printed form - Only re-add transaction after editing enrollment if the enrollment is not free - Settle positive and negative enrollment transactions against eachother when editing enrollment or unenrolling - Fix n.a. abbreviation to N/A --- amelie/activities/forms.py | 5 +- .../migrations/0014_alter_activity_price.py | 20 ++++ amelie/activities/models.py | 36 ++++++- .../templates/activity_enrollment_print.html | 21 ++-- .../activity_enrollment_overview.html | 6 +- .../includes/activity_participants_table.html | 13 ++- amelie/activities/views.py | 65 +++++++++--- amelie/api/activitystream.py | 1 - amelie/calendar/models.py | 19 +++- .../0024_delete_payment_delete_paymenttype.py | 2 +- amelie/members/models.py | 2 +- .../members/templates/person_membership.html | 2 +- amelie/personal_tab/transactions.py | 98 ++++++++++++------- amelie/settings/generic.py | 2 + 14 files changed, 218 insertions(+), 74 deletions(-) create mode 100644 amelie/activities/migrations/0014_alter_activity_price.py diff --git a/amelie/activities/forms.py b/amelie/activities/forms.py index 2b15bf86..3acf98d0 100644 --- a/amelie/activities/forms.py +++ b/amelie/activities/forms.py @@ -70,8 +70,11 @@ def clean(self): if not self.cleaned_data.get('price', None): self.cleaned_data['price'] = 0 + if self.cleaned_data["price"] < Decimal("0.00") and self.cleaned_data["enrollment"]: + raise forms.ValidationError(_l("Website enrolment must have a positive price. Either turn off the ability to enrol or increase the cost of the activity.")) + if self.cleaned_data["price"] > settings.PERSONAL_TAB_MAXIMUM_ACTIVITY_PRICE and self.cleaned_data["enrollment"]: - raise forms.ValidationError(_l("Website enrolment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity.").format(settings.PERSONAL_TAB_MAXIMUM_ACTIVITY_PRICE)) + raise forms.ValidationError(_l("Website enrollment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity.").format(settings.PERSONAL_TAB_MAXIMUM_ACTIVITY_PRICE)) if self.cleaned_data["enrollment"]: if not self.cleaned_data.get('enrollment_begin', None): diff --git a/amelie/activities/migrations/0014_alter_activity_price.py b/amelie/activities/migrations/0014_alter_activity_price.py new file mode 100644 index 00000000..2776f709 --- /dev/null +++ b/amelie/activities/migrations/0014_alter_activity_price.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.12 on 2026-08-12 13:36 + +import django.core.validators +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0013_activity_enrollment_private'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='price', + field=models.DecimalField(decimal_places=2, default='0.00', help_text='Enrolled participants will be emailed when you change the price.', max_digits=8, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='price'), + ), + ] diff --git a/amelie/activities/models.py b/amelie/activities/models.py index 3f70aec2..b23d9f03 100644 --- a/amelie/activities/models.py +++ b/amelie/activities/models.py @@ -22,6 +22,7 @@ from amelie.calendar.managers import EventManager from amelie.calendar.models import Event, Participation from amelie.members.models import Person +from amelie.personal_tab import transactions from amelie.tools.discord import send_discord, send_discord_presave from amelie.tools.managers import SubclassManager @@ -70,7 +71,10 @@ class Activity(Event): photos = models.ManyToManyField(Attachment, blank=True, related_name='foto_set') components = models.ManyToManyField('self', blank=True) - price = models.DecimalField(default="0.00", max_digits=8, decimal_places=2, verbose_name=_l('price'), help_text=_l("Enrolled participants will be emailed when you change the price.")) + price = models.DecimalField( + default="0.00", max_digits=8, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))], + verbose_name=_l('price'), help_text=_l("Enrolled participants will be emailed when you change the price.") + ) can_unenroll = models.BooleanField(default=True, verbose_name=_l('can unenroll')) image_icon = models.ImageField(upload_to='activities/icon/', max_length=255, null=True, blank=True, verbose_name=_l('icon'), help_text=_l('Image of 175 by 275 pixels.')) @@ -183,7 +187,7 @@ def has_costs(self) -> bool: a discount. """ # Trivial check - if self.price != 0: + if self.price != Decimal("0.00"): return True # Check every option @@ -327,7 +331,6 @@ def update_waiting_list(self) -> List[Participation]: activity_send_enrollmentmail(new_participation, from_waiting_list=True) new_participation.waiting_list = False new_participation.save() - from amelie.personal_tab import transactions transactions.add_participation(new_participation, added_by=new_participation.added_by) new_participants.append(new_participation) return new_participants @@ -341,7 +344,6 @@ def unenroll_all_participants(self, actor: Optional[Person] = None): :type actor: Person """ # Undo transactions and remove participations - from amelie.personal_tab import transactions for participation in self.participation_set.all(): # If necessary, compensate the person for the enrollment costs. with translation.override(participation.person.preferred_language): @@ -410,6 +412,10 @@ def save(self, *args, **kwargs): def has_extra_costs(self): return False + def get_min_max_extra_costs(self) -> Tuple[Decimal, Decimal]: + """Gets the minimum possible costs and maximum possible costs for this option.""" + return Decimal('0.00'), Decimal('0.00') + def as_leaf_class(self, enforce_content_type=None): if enforce_content_type is not None: content_type = enforce_content_type @@ -457,6 +463,12 @@ class EnrollmentoptionCheckbox(Enrollmentoption): def has_extra_costs(self): return bool(self.price_extra) + def get_min_max_extra_costs(self) -> Tuple[Decimal, Decimal]: + if self.has_extra_costs(): + return min(self.price_extra, Decimal('0.00')), max(self.price_extra, Decimal('0.00')) + else: + return super().get_min_max_extra_costs() + def has_limit(self): return self.maximum != 0 @@ -484,6 +496,15 @@ class EnrollmentoptionNumeric(Enrollmentoption): def has_extra_costs(self): return bool(self.price_extra) + def get_min_max_extra_costs(self) -> Tuple[Decimal, Decimal]: + if self.has_extra_costs(): + # 9999 is a highball estimate that should not be reached in normal use, because 'infinite' is hard to work with. + max_choice_amount = (self.maximum_per_person or self.maximum or 9999) + largest_extra_price = self.price_extra * max_choice_amount + return min(largest_extra_price, Decimal('0.00')), max(largest_extra_price, Decimal('0.00')) + else: + return super().get_min_max_extra_costs() + def has_limit(self): return self.maximum != 0 @@ -508,6 +529,13 @@ class EnrollmentoptionSelectbox(Enrollmentoption): def has_extra_costs(self): return any([selectboxoption.price_extra for selectboxoption in self.selectboxoption_set.all()]) + def get_min_max_extra_costs(self) -> Tuple[Decimal, Decimal]: + if self.has_extra_costs(): + possible_prices = [selectboxoption.price_extra for selectboxoption in self.selectboxoption_set.all()] + return min(min(possible_prices), Decimal('0.00')), max(max(possible_prices), Decimal('0.00')) + else: + return super().get_min_max_extra_costs() + class SelectboxOption(models.Model): """ diff --git a/amelie/activities/templates/activity_enrollment_print.html b/amelie/activities/templates/activity_enrollment_print.html index 822c11a5..8cebfaae 100644 --- a/amelie/activities/templates/activity_enrollment_print.html +++ b/amelie/activities/templates/activity_enrollment_print.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} +

    + {% blocktrans with name=obj.summary date=obj.begin|date:"Y-m-d" %}Enrollments for {{ name }} ({{ date }}){% endblocktrans %}
    + {% blocktrans with date=print_date|date:"Y-m-d H:i" %}(printed on {{ date }}){% endblocktrans %} +

    +

    {% trans 'This enrollment list contains personal data. Please do not leave this lying around somewhere and make sure that it is destroyed after use. If you find this list somewhere, please give it to the board of Inter-Actief.' %}

    @@ -46,25 +52,24 @@ {% endfor %} {% if participation.is_free %} - {% trans 'n.a. (free)' %} + {% trans 'N/A (free)' %} {% else %} {% with participation.payment_details as details %} - {% if participation.is_paid %} - {% trans 'Paid' %} + {% trans 'Paid' %} {# Cannot use brackets in jinja templates, so we need to list the same block twice. #} {% elif details.0 == 'unpaid' and details.1 != None %} - {% trans 'To be debited' %} + {% trans 'To be debited' %} {% elif details.0 == 'reversed' %} - {% trans 'To be debited' %} + {% trans 'To be debited' %} {% else %} {% if details.0 == 'unpaid' and details.1 == None %} - {% trans 'Needs manual payment' %} + {% trans 'Needs manual payment' %} {% else %} - {% trans 'Not paid yet' %} + {% trans 'Not paid yet' %} {% endif %} +
    {% blocktrans with costs=participation.to_be_paid_costs|intcomma %}Open amount: € {{ costs }}{% endblocktrans %} {% endif %} -
    {% endwith %} {% endif %} diff --git a/amelie/activities/templates/includes/activity_enrollment_overview.html b/amelie/activities/templates/includes/activity_enrollment_overview.html index 2c3c3ba3..705d6eda 100644 --- a/amelie/activities/templates/includes/activity_enrollment_overview.html +++ b/amelie/activities/templates/includes/activity_enrollment_overview.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} {% block head %} @@ -168,12 +169,13 @@

    {% blocktrans with n=number_waitin {% endfor %} {% if participation.is_free %} - {% trans 'n.a. (free)' %} + {% trans 'N/A (free)' %} {% else %} {% if participation.person.has_mandate_activities %} {% trans 'To be debited' %} {% else %} - {% trans 'Needs manual payment' %} + {% trans 'Needs manual payment' %}
    + {% blocktrans with costs=participation.to_be_paid_costs|intcomma %}Open amount: € {{ costs }}{% endblocktrans %} {% endif %} {% endif %} diff --git a/amelie/activities/templates/includes/activity_participants_table.html b/amelie/activities/templates/includes/activity_participants_table.html index 586b7263..9abebe14 100644 --- a/amelie/activities/templates/includes/activity_participants_table.html +++ b/amelie/activities/templates/includes/activity_participants_table.html @@ -1,3 +1,4 @@ +{% load humanize %} {% load i18n %} @@ -61,10 +62,12 @@ {% if request.is_board or is_roomduty %} - + diff --git a/amelie/api/test_activitystream.py b/amelie/api/test_activitystream.py index 8ece320d..064d1d0d 100644 --- a/amelie/api/test_activitystream.py +++ b/amelie/api/test_activitystream.py @@ -290,7 +290,7 @@ def test_invalid_token(self): for option in self.activity.enrollmentoption_set.all(): option.delete() - self.send_and_compare_request_error('activitySignup', [self.activity.pk, 0.00, []], 'invalidToken', -32603) + self.send_and_compare_request_error('activitySignup', [self.activity.pk, 0.00, []], 'invalidToken', -32098) def test_free(self): # Clear options diff --git a/pyproject.toml b/pyproject.toml index 14806502..52426656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "django-environ>=0.13.0,<0.15", # Django Modern RPC -- For our JSONRPC API - "django-modern-rpc>=2.0.0,<2.1", + "django-modern-rpc>=2.1.0,<2.2", # WYSIWYG Editor for Django "django-ckeditor>=6.7.3", From 0f69f918861e7b3e0489a942c1d2cd9266390db0 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Sat, 15 Aug 2026 10:03:08 +0200 Subject: [PATCH 30/39] Add declaration submitter name and date to subject to avoid grouping in Gmail --- amelie/personal_tab/forms.py | 7 ++++++- amelie/personal_tab/templates/declaration/declaration.mail | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/amelie/personal_tab/forms.py b/amelie/personal_tab/forms.py index 5165bd5f..55df36b1 100644 --- a/amelie/personal_tab/forms.py +++ b/amelie/personal_tab/forms.py @@ -5,6 +5,7 @@ from typing import Optional, List from django.forms.models import ModelChoiceIterator +from django.utils.timezone import get_current_timezone from localflavor.generic.forms import BICFormField, IBANFormField from django import forms @@ -595,7 +596,11 @@ def save(self, request): reply_to = settings.TREASURER_EMAIL # Prepare context for the email - context = {'declaration': declaration} + context = { + 'declaration': declaration, + 'submitter_name': person.incomplete_name(), + 'submission_date': declaration.submission_date.astimezone(get_current_timezone()) + } # Generate PDF of the declaration form and add it to the attachments pdf = declaration.get_pdf() diff --git a/amelie/personal_tab/templates/declaration/declaration.mail b/amelie/personal_tab/templates/declaration/declaration.mail index 6d210534..3dcbe156 100644 --- a/amelie/personal_tab/templates/declaration/declaration.mail +++ b/amelie/personal_tab/templates/declaration/declaration.mail @@ -1,4 +1,4 @@ -{% load attach i18n only subject absolute_url htmlify%}{% comment %} Whitespace IS important in this file {% endcomment %}{% comment %} Copy of email_basic, because the logo must be removed. This is to prevent the logo from being saved in the bookkeeping for every declaration. {% endcomment %}{% subject %}{% blocktrans %}Declaration submitted via Amélie{% endblocktrans %}{% endsubject %}{% onlyhtml %} +{% load attach i18n only subject absolute_url htmlify%}{% comment %} Whitespace IS important in this file {% endcomment %}{% comment %} Copy of email_basic, because the logo must be removed. This is to prevent the logo from being saved in the bookkeeping for every declaration. {% endcomment %}{% subject %}{% blocktrans with name=submitter_name date=submission_date|date:"Y-m-d H:i" %}Declaration submitted via Amélie by {{name}} on {{date}}{% endblocktrans %}{% endsubject %}{% onlyhtml %} From 8b81754d854123f60a84b9bf9c9f8b26410c4417 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:05:04 +0000 Subject: [PATCH 31/39] build(deps): update sentry-sdk requirement Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/2.54.0...2.68.0) --- updated-dependencies: - dependency-name: sentry-sdk dependency-version: 2.68.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52426656..a171e056 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ dependencies = [ "mozilla-django-oidc>=5.0.2,<5.1", # Sentry error logging - "sentry-sdk>=2.54.0,<2.67", + "sentry-sdk>=2.54.0,<2.69", # Health checks for Django "django-health-check[psutil,celery,rabbitmq,redis]>=4.0,<4.5", From 5fc300515d60fe237cc04a69088fd6366b58478a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:05:10 +0000 Subject: [PATCH 32/39] build(deps): update httplib2 requirement Updates the requirements on [httplib2](https://github.com/httplib2/httplib2) to permit the latest version. - [Changelog](https://github.com/httplib2/httplib2/blob/master/CHANGELOG) - [Commits](https://github.com/httplib2/httplib2/compare/v0.20.4...v0.32.0) --- updated-dependencies: - dependency-name: httplib2 dependency-version: 0.32.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52426656..bd1d9f1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dependencies = [ "nh3>=0.3.3,<0.4", # Other random dependencies, to be documented, see issue #556 - "httplib2>=0.20.4,<0.32", + "httplib2>=0.20.4,<0.33", "icalendar>=7.0.3,<7.3", "Markdown>=3.10.2,<3.11", "mysqlclient>=2.2.8", From 57d177fca46c845332c2805f72f48a9704eeba60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:05:17 +0000 Subject: [PATCH 33/39] build(deps): update django-debug-toolbar requirement Updates the requirements on [django-debug-toolbar](https://github.com/django-commons/django-debug-toolbar) to permit the latest version. - [Release notes](https://github.com/django-commons/django-debug-toolbar/releases) - [Changelog](https://github.com/django-commons/django-debug-toolbar/blob/main/docs/changes.rst) - [Commits](https://github.com/django-commons/django-debug-toolbar/compare/3.7...7.1.0) --- updated-dependencies: - dependency-name: django-debug-toolbar dependency-version: 7.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52426656..24d90037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ dependencies = [ "pyOpenSSL>=26.0.0,<26.5.0", # Development - "django-debug-toolbar>=3.7,<7.1", + "django-debug-toolbar>=3.7,<7.2", "django-rosetta>=0.10.3,<0.11", "docutils>=0.19,<0.23", "pyinotify", From 8085d0376ea18df6d0177fca77377674ff6718fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:17:54 +0000 Subject: [PATCH 34/39] build(deps): update docutils requirement Updates the requirements on [docutils](https://github.com/rtfd/recommonmark) to permit the latest version. - [Changelog](https://github.com/readthedocs/recommonmark/blob/master/CHANGELOG.md) - [Commits](https://github.com/rtfd/recommonmark/commits) --- updated-dependencies: - dependency-name: docutils dependency-version: '0.23' dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 79770f66..d5ce9c6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ dependencies = [ # Development "django-debug-toolbar>=3.7,<7.2", "django-rosetta>=0.10.3,<0.11", - "docutils>=0.19,<0.23", + "docutils>=0.19,<0.24", "pyinotify", "model_bakery>=1.23.3,<1.25", "django-silk>=5.5.0,<5.6", From e7aebbfe2089c74a79a1417ac9c0fa4d92a8e5d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:17:59 +0000 Subject: [PATCH 35/39] build(deps): update django-health-check requirement Updates the requirements on [django-health-check](https://github.com/codingjoe/django-health-check) to permit the latest version. - [Release notes](https://github.com/codingjoe/django-health-check/releases) - [Commits](https://github.com/codingjoe/django-health-check/compare/4.0.0...4.5.0) --- updated-dependencies: - dependency-name: django-health-check dependency-version: 4.5.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 79770f66..1efd8d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,7 +118,7 @@ dependencies = [ "sentry-sdk>=2.54.0,<2.69", # Health checks for Django - "django-health-check[psutil,celery,rabbitmq,redis]>=4.0,<4.5", + "django-health-check[psutil,celery,rabbitmq,redis]>=4.0,<4.6", # Allows for CAPTCHA support in forms "django-simple-captcha", From 9beb7fa0cc27c1103c66fda5bb39c73709313dc7 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 17 Aug 2026 14:54:24 +0200 Subject: [PATCH 36/39] Add registration overview showing new members in the current academic year, to track enrollment status --- amelie/members/members_urls.py | 1 + amelie/members/models.py | 14 +++++ .../templates/preregistration_status.html | 12 +++- .../templates/registration_status.html | 59 +++++++++++++++++++ amelie/members/views.py | 22 ++++++- 5 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 amelie/members/templates/registration_status.html diff --git a/amelie/members/members_urls.py b/amelie/members/members_urls.py index 5f7a2ecc..e5644361 100644 --- a/amelie/members/members_urls.py +++ b/amelie/members/members_urls.py @@ -55,6 +55,7 @@ path('preregistration/', views.PreRegistrationStatus.as_view(), name='preregistration_status'), path('preregistration/print/', views.PreRegistrationPrintAll.as_view(), name='preregistration_print_all'), path('preregistration/print/dogroup/', views.PreRegistrationPrintDogroup.as_view(), name='preregistration_print_dogroup'), + path('registration/', views.RegistrationStatus.as_view(), name='registration_status'), path('registration_form///', views.registration_form, name='registration_form'), path('membership_form///', views.membership_form, name='membership_form'), diff --git a/amelie/members/models.py b/amelie/members/models.py index 27b2dfae..1214737e 100644 --- a/amelie/members/models.py +++ b/amelie/members/models.py @@ -428,6 +428,14 @@ def is_employee(self): def is_student(self): return hasattr(self, 'student') + @property + def dogroup(self): + if self.is_student(): + for period in self.student.studyperiod_set.all().order_by('-begin'): + if period.dogroup: + return period.dogroup + return None + def is_active_member(self): return self.function_set.filter(end__isnull=True, committee__abolished__isnull=True).exists() @@ -524,6 +532,12 @@ def initials_last_name(self): else: return ' '.join([first_name, self.last_name]) + def sortable_name(self): + first_name = self.first_name + if not self.first_name and self.initials: + first_name = self.initials + return ', '.join([self.last_name, first_name]) + def current_committees(self): """ Returns a queryset with all committees this person is currently in. diff --git a/amelie/members/templates/preregistration_status.html b/amelie/members/templates/preregistration_status.html index bda4a7c9..c689bb0f 100644 --- a/amelie/members/templates/preregistration_status.html +++ b/amelie/members/templates/preregistration_status.html @@ -23,7 +23,15 @@

    {% trans 'Pre-enrollment status' %}

    {% trans 'These are the current pre-enrollments that are not activated yet, per do-group.' %}

    - {% trans 'If the member has signed their membership and authorization forms, the membership and any authorizations should automatically get activated and it will disappear from this list.' %}
    + {% trans 'If the member has signed their membership and authorization forms, the membership and any authorizations should automatically get activated and it will disappear from this list.' %} +

    +

    + {% url 'members:registration_status' as enrollment_url %} + {% blocktrans with url=enrollment_url %} + To view already activated memberships from this academic year, check the current enrollment status instead. + {% endblocktrans %} +

    +

    {% trans 'You can (re-)print the forms for that member by clicking the "Print" () button.' %} {% if request.is_board %} {% trans 'Additionally, as a board member, you can view the state of the signature requests (), and attempt re-download of the signed forms () or even re-send them () if it failed for whatever reason.' %} @@ -103,7 +111,7 @@

    {{ dogroup.grouper }} - {% blocktrans with len=dogroup.list|length %}{{ len

    {% trans 'No pre-enrollments' %}

    - {% trans 'There are currently no pre-enrollments.' %} + {% trans 'There are currently no pending pre-enrollments.' %}

    diff --git a/amelie/members/templates/registration_status.html b/amelie/members/templates/registration_status.html new file mode 100644 index 00000000..0b3e9b8c --- /dev/null +++ b/amelie/members/templates/registration_status.html @@ -0,0 +1,59 @@ +{% extends "basis.html" %} +{% load i18n form_stylish %} + +{% block titel %} + {% trans 'Enrollment status' %} +{% endblock titel %} + +{% block content %} +
    +
    +

    {% trans 'Enrollment status' %}

    +
    +

    + {% trans 'These are the new members this association year, per do-group. These memberships are already activated.' %} +

    +

    + {% url 'members:preregistration_status' as pre_enrollment_url %} + {% blocktrans with url=pre_enrollment_url %} + To view unactivated memberships, check the current pre-enrollment status instead. + {% endblocktrans %} +

    +
    +
    +
    + + {% regroup memberships by member.dogroup as memberships_by_dogroup %} + + {% for dogroup in memberships_by_dogroup %} +
    +
    +

    {{ dogroup.grouper }} - {% blocktrans with len=dogroup.list|length %}{{ len }} member(s){% endblocktrans %}

    +
    +
    {% if participation.is_free %} - {% trans 'n.a. (free)' %} + {% trans 'N/A (free)' %}   {% if participation.activitytransaction_set.exists %} - + {% if participation.unpaid_transactions|length != 0 %} + + {% endif %} @@ -115,7 +118,7 @@ {% elif details.0 == 'unpaid' and details.1 == None %}
    - {% trans 'Needs manual payment' %} + {% blocktrans with costs=participation.to_be_paid_costs|intcomma %}Needs manual payment (€ {{ costs }}){% endblocktrans %} {% endif %} {% if details.0 == 'unpaid' and details.1 == None %} @@ -131,7 +134,7 @@ {% elif is_committee %}
    {% if participation.is_free %} - {% trans 'n.a. (free)' %} + {% trans 'N/A (free)' %} {% else %} {% with participation.payment_details as details %} {% if participation.is_paid %} @@ -145,7 +148,7 @@ {% trans 'Not paid yet' %} {% if details.0 == 'unpaid' and details.1 == None %}
    - {% trans 'Needs manual payment' %} + {% blocktrans with costs=participation.to_be_paid_costs|intcomma %}Needs manual payment (€ {{ costs }}){% endblocktrans %} {% endif %} {% endif %} diff --git a/amelie/activities/views.py b/amelie/activities/views.py index 62a17a56..0bf1d8b3 100644 --- a/amelie/activities/views.py +++ b/amelie/activities/views.py @@ -16,7 +16,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied, ImproperlyConfigured +from django.core.exceptions import PermissionDenied, ImproperlyConfigured, ValidationError from django.core.paginator import EmptyPage, PageNotAnInteger from django.forms import Form from django.urls import reverse, reverse_lazy @@ -47,6 +47,7 @@ from amelie.members.forms import PersonSearchForm from amelie.members.models import Person, Photographer from amelie.members.query_forms import ActivityMailingForm +from amelie.personal_tab import transactions from amelie.personal_tab.models import ActivityTransaction from amelie.tools import amelie_messages, types from amelie.tools.const import TaskPriority @@ -57,6 +58,7 @@ RequireCommitteeMixin, RequireMemberMixin from amelie.tools.paginator import RangedPaginator + logger = logging.getLogger(__name__) @@ -402,7 +404,7 @@ def dispatch(self, request, *args, **kwargs): self.person: Person = request.person # Indirect action is only allowed by board members or roomduty members - is_roomduty = request.person.is_room_duty() + is_roomduty = hasattr(request, 'person') and request.person.is_room_duty() if self.indirect and not (self.activity.can_edit(request.person) or is_roomduty): messages.error(request, self.indirect_permisison_error_msg()) return redirect(self.activity) @@ -560,7 +562,6 @@ def form_valid(self, form): return render(self.request, "activity_enrollment_mandate.html", {'activity': self.activity}) if price and (force_skip_waiting_list or not self.activity.enrollment_full): - from amelie.personal_tab import transactions transactions.add_participation(participation=participation, added_by=self.request.person) # Maybe send a notification e-mail to the person and set a status message in the request. @@ -597,7 +598,6 @@ def form_valid(self, form): Participation.objects.select_for_update().get(pk=participation.pk) # If necessary, compensate the person for the enrollment costs. - from amelie.personal_tab import transactions transactions.remove_participation(participation) # Delete the participation @@ -688,7 +688,6 @@ def set_enrolled_status_message(self, participation: Participation): @transaction.atomic def form_valid(self, form): - from amelie.personal_tab import transactions # It is important to NOT delete the Participation, because the person's spot in the activity # or on the waiting list could be given away to the next waiting list entry. # So we need to update the enrollment options of the participation in-place @@ -710,7 +709,10 @@ def form_valid(self, form): answer.save() subform.save_m2m() - if not participation.waiting_list: + # If this is a regular Participation (not waiting list) and it has costs after editing, we need to (re-)add + # the transaction to update the costs on the personal tab + price, with_enrollmentoptions = participation.calculate_costs() + if price and not participation.waiting_list: # If this is a regular Participation (not waiting list), we need to re-add # the transaction to update the costs on the personal tab transactions.add_participation(participation, added_by=self.request.person, is_edited_participation=True) @@ -1157,6 +1159,18 @@ def form_valid(self, form): old_obj = self.get_object() new_obj = form.instance + # The total possible activity price may not be negative, or it will interfere with the payment processing. + # So, if the base activity cost, plus the costs of all negatively priced enrollment options, is lower than 0, + # we raise a validation error. + activity_base_price = new_obj.price + enrollment_options_min_prices = sum(o.get_min_max_extra_costs()[0] for o in new_obj.enrollmentoption_set.all()) + if activity_base_price + enrollment_options_min_prices < 0: + form.add_error('price', ValidationError( + _("Editing the activity price to this value would make the minimum possible price for this activity negative! " + "This is not allowed. Please modify the pricing and limits of the enrollment options first.") + )) + return self.form_invalid(form) + # Save changes to the parent activity so new transactions are calculated correctly in case of a new price/date response = super().form_valid(form) @@ -1176,7 +1190,6 @@ def form_valid(self, form): if old_obj is not None and needs_new_transactions: # Undo and create new transactions - from amelie.personal_tab import transactions for participation in old_obj.participation_set.all(): # Note - reason text is max 200 chars! reason = _("Activity '{activity}' was changed (reversal of old transaction) ({change_reasons})").format( @@ -1249,11 +1262,26 @@ def get_form_class(self): return ENROLLMENTOPTION_FORM_TYPES.get(self.model, None) def form_valid(self, form): - self.object = form.save(commit=False) - self.object.activity = self.get_activity() + self.object: Enrollmentoption = form.save(commit=False) self.object.content_type = ContentType.objects.get_for_model(self.model) + activity = self.get_activity() + + # The total possible activity price may not be negative, or it will interfere with the payment processing. + # So, if the base activity cost, plus the costs of all negatively priced enrollment options, is lower than 0, + # we raise a validation error. + activity_base_price = activity.price + existing_enrollment_options_min_prices = sum(o.get_min_max_extra_costs()[0] for o in activity.enrollmentoption_set.all()) + new_option_min_price = self.object.get_min_max_extra_costs()[0] + if activity_base_price + existing_enrollment_options_min_prices + new_option_min_price < 0: + form.add_error(None, ValidationError( + _("Adding this option would make the minimum possible price for this activity negative! This is not " + "allowed. Please modify the pricing or limits of this or other enrollment options first.") + )) + return self.form_invalid(form) - return super(EnrollmentoptionCreateView, self).form_valid(form) + self.object.activity = activity + self.object.save() + return HttpResponseRedirect(self.get_success_url()) def get(self, request, *args, **kwargs): activity_ins = self.get_activity() @@ -1290,7 +1318,20 @@ def form_valid(self, form): raise Exception("Updating requires a person") old_obj = self.get_object() - new_obj = form.instance + new_obj: Enrollmentoption = form.instance + + # The total possible activity price may not be negative, or it will interfere with the payment processing. + # So, if the base activity cost, plus the costs of all negatively priced enrollment options, is lower than 0, + # we raise a validation error. + activity_base_price = old_obj.activity.price + existing_enrollment_options_min_prices = sum(o.get_min_max_extra_costs()[0] for o in old_obj.activity.enrollmentoption_set.all() if o != old_obj) + new_option_min_price = new_obj.get_min_max_extra_costs()[0] + if activity_base_price + existing_enrollment_options_min_prices + new_option_min_price < 0: + form.add_error(None, ValidationError( + _("Editing this option in this way would make the minimum possible price for this activity negative! " + "This is not allowed. Please modify the pricing or limits of this or other enrollment options first.") + )) + return self.form_invalid(form) # Save changes to the parent activity so new transactions are calculated correctly in case of a new price/date response = super().form_valid(form) @@ -1307,7 +1348,6 @@ def form_valid(self, form): if old_obj is not None and needs_new_transactions: # Undo and create new transactions - from amelie.personal_tab import transactions for participation in old_obj.activity.participation_set.all(): # Note - reason text is max 200 chars! reason = _("An enrollment option for activity '{activity}' was changed (reversal of old transaction) ({change_reasons})").format( @@ -1448,6 +1488,7 @@ def export_print(request, pk): participation_set = obj.confirmed_participations.order_by('person__first_name') number_participants = obj.confirmed_participants.count() if obj.confirmed_participants else 0 + print_date = timezone.now() return render(request, "activity_enrollment_print.html", locals()) @staticmethod diff --git a/amelie/api/activitystream.py b/amelie/api/activitystream.py index dc3456b1..0639a6a0 100644 --- a/amelie/api/activitystream.py +++ b/amelie/api/activitystream.py @@ -786,7 +786,6 @@ def activity_signup_revoke(activity_id: int, ctx: RpcRequestContext = None, **kw ) # If necessary, compensate the person for the enrollment costs. - from amelie.personal_tab import transactions transactions.remove_participation(participation) # Delete the participation diff --git a/amelie/calendar/models.py b/amelie/calendar/models.py index 7679de6e..ff985cab 100644 --- a/amelie/calendar/models.py +++ b/amelie/calendar/models.py @@ -66,7 +66,7 @@ def is_paid(self): # but they should all be compensated by a negative ActivityTransaction, thus they can be ignored. # Newest (by creation date) positive (or 0) price transaction - current_transaction = self.activitytransaction_set.filter(price__gte=0).order_by('-added_on').first() + current_transaction = self.activitytransaction_set.filter(price__gte=0).order_by('-added_on', '-pk').first() if current_transaction is not None: return current_transaction.is_paid() else: @@ -86,8 +86,8 @@ def payment_details(self): if self.is_free(): return 'free', None - current_transaction = self.activitytransaction_set.filter(price__gte=0).order_by('-added_on').first() - return get_transaction_payment_status(transaction=current_transaction, mandate_type='activities') + current_transaction = self.activitytransaction_set.filter(price__gte=0).order_by('-added_on', '-pk').first() + return get_transaction_payment_status(_transaction=current_transaction, mandate_type='activities') def calculate_costs(self) -> Tuple[Decimal, bool]: from amelie.activities.models import Activity @@ -106,6 +106,17 @@ def calculate_costs(self) -> Tuple[Decimal, bool]: total_costs = activity.price + sum(prices_extra) return total_costs, len(prices_extra) > 0 + @property + def unpaid_transactions(self): + return [t for t in self.activitytransaction_set.all() if not t.is_paid()] + + @property + def to_be_paid_costs(self): + """ + Calculate the open costs for this participation, considering all transactions that relate to it. + """ + return sum(t.price for t in self.unpaid_transactions) or Decimal("0.00") + @transaction.atomic def mark_as_paid(self, payment_method, actor: Optional[Person] = None): """ @@ -115,7 +126,7 @@ def mark_as_paid(self, payment_method, actor: Optional[Person] = None): from amelie.personal_tab.models import ManualPaymentSettlement # Get the ActivityTransaction(s) for this Participation that still need to be paid - ats = [t for t in self.activitytransaction_set.all() if not t.is_paid()] + ats = self.unpaid_transactions if not ats: return None diff --git a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py index 341306c1..8b15fdd7 100644 --- a/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py +++ b/amelie/members/migrations/0024_delete_payment_delete_paymenttype.py @@ -124,7 +124,7 @@ def forwards(apps, schema_editor): num_left_unpaid = 0 created_internal_settlements = 0 try: - authorization_payment_type = PaymentType.objects.get(pk=4) # Annual authorization + authorization_payment_type = PaymentType.objects.get(name="Annual authorization") # Annual authorization except PaymentType.DoesNotExist: authorization_payment_type = None for i, membership in enumerate(Membership.objects.all().order_by('pk')): diff --git a/amelie/members/models.py b/amelie/members/models.py index 94256c8a..27b2dfae 100644 --- a/amelie/members/models.py +++ b/amelie/members/models.py @@ -1039,7 +1039,7 @@ def payment_details(self): return 'free', None current_transaction = self.payment_transaction() - return get_transaction_payment_status(transaction=current_transaction, mandate_type='contribution') + return get_transaction_payment_status(_transaction=current_transaction, mandate_type='contribution') @transaction.atomic def create_contribution_transaction(self, date: Optional[datetime.datetime] = None): diff --git a/amelie/members/templates/person_membership.html b/amelie/members/templates/person_membership.html index e9069a60..224e921c 100644 --- a/amelie/members/templates/person_membership.html +++ b/amelie/members/templates/person_membership.html @@ -69,7 +69,7 @@
    {% if membership.is_free %} - {% trans 'n.a.' %} + {% trans 'N/A' %}   {% if membership.contributiontransaction_set.exists %} diff --git a/amelie/personal_tab/transactions.py b/amelie/personal_tab/transactions.py index 3a9fe08e..b41c2561 100644 --- a/amelie/personal_tab/transactions.py +++ b/amelie/personal_tab/transactions.py @@ -4,54 +4,82 @@ from typing import Optional, Tuple, Union, List, Dict, Any from django.conf import settings +from django.db import transaction from django.db.models import Sum, Q from django.utils import timezone, translation from django.utils.translation import gettext as _ from amelie.members.models import Study, Student, Person from amelie.personal_tab.models import ActivityTransaction, DiscountPeriod, DiscountCredit, CookieCornerTransaction, \ - Discount, Transaction, PersonalTabSettlement, ReversalTransaction, Authorization + Discount, Transaction, PersonalTabSettlement, ReversalTransaction, Authorization, ManualPaymentSettlement, \ + PaymentMethod from amelie.tools.logic import current_academic_year_strict, current_association_year +@transaction.atomic def participation_transaction(participation, reason, cancel=False, added_by=None): """ Helper function that adds an ActivityTransaction. participation: Object to generate a transaction for. reason: Reason/description of this transaction - cancel: If True, make a compensation for a participation. This requires the participation.pk != None. + cancel: If True, make a compensation for the participation. This requires the participation.pk != None. added_by: Who created the transaction. """ - # Take the begin date/time as the date of the transaction, unless this moment has already passed. Jelte 2013-09-25 + # Take the beginning date/time as the date of the transaction, unless this moment has already passed. Jelte 2013-09-25 date = max(participation.event.begin, timezone.now()) if cancel: - # The latest (by create time) positive ActivityTransaction for this participation should be compensated - old_transaction = ActivityTransaction.objects.filter( + # The latest (by creation time) positive ActivityTransaction for this participation should be compensated + old_transactions = ActivityTransaction.objects.filter( participation=participation, event=participation.event, person=participation.person, price__gte=0 # Positive (or 0) price - ).order_by('-added_on').first() # Latest transaction, by creation time, or None if none exists + ).order_by('-added_on', '-pk') + old_transaction = old_transactions.first() # Latest transaction, by creation time, or None if none exists if old_transaction is None: # A compensation does not need to be created because there is no transaction to compensate. return - price = -1 * old_transaction.price - with_enrollment_options = old_transaction.with_enrollment_options - else: - price, with_enrollment_options = participation.calculate_costs() + if old_transactions.filter(added_on=old_transaction.added_on).count() > 1: + raise ValueError("Found multiple ActivityTransaction to cancel against.") - # Create transaction - transaction = ActivityTransaction(price=price, description=reason, participation=participation, - event=participation.event, person=participation.person, date=date, - with_enrollment_options=with_enrollment_options, added_by=added_by) + # Create reversal activity transaction + tr = ActivityTransaction.objects.create( + price=(-1 * old_transaction.price), + description=reason, participation=participation, + event=participation.event, person=participation.person, date=date, + with_enrollment_options=old_transaction.with_enrollment_options, added_by=added_by + ) - # And save - transaction.save() + # Create a settlement to pay the old transaction by cancelling it out against the new reversal transaction. + payment_method = PaymentMethod.objects.get(pk=settings.INTERNAL_SETTLEMENT_PAYMENT_METHOD_ID) + # Translate the description to the user's preferred language + with translation.override(participation.person.preferred_language): + settlement_description = _( + "Settlement of previous costs for {activity}").format( + activity=participation.event.summary + ) + ManualPaymentSettlement.create_for_transactions( + transactions=[old_transaction, tr], + payment_method=payment_method, + person=participation.person, + settlement_description=settlement_description, + payment_description=settlement_description, + created_by=added_by, + ) + else: + price, with_enrollment_options = participation.calculate_costs() + # Create regular activity transaction + ActivityTransaction.objects.create( + price=price, description=reason, participation=participation, + event=participation.event, person=participation.person, date=date, + with_enrollment_options=with_enrollment_options, added_by=added_by + ) +@transaction.atomic def add_participation(participation, added_by=None, is_edited_participation=False, reason=None): """Adds a transaction for a participation in an event.""" @@ -67,6 +95,7 @@ def add_participation(participation, added_by=None, is_edited_participation=Fals participation_transaction(participation, reason, added_by=added_by) +@transaction.atomic def remove_participation(participation, added_by=None, is_edited_participation=False, reason=None): """Adds a transaction to nullify a participation in an event.""" @@ -146,10 +175,10 @@ def cookie_corner_sale(article, amount, person, added_by): DiscountCredit(discount_period=discount_period, date=now, price=-discount_amount, person=person, description=description, discount=discount, added_by=added_by).save() - transaction = CookieCornerTransaction(date=now, price=total_price, person=person, description=description, + tr = CookieCornerTransaction(date=now, price=total_price, person=person, description=description, discount=discount, article=article, amount=amount, added_by=added_by) - transaction.save() - return transaction + tr.save() + return tr def free_cookie_discount(): @@ -206,6 +235,7 @@ def free_cookies_allowed(article): return article in articles +@transaction.atomic def free_cookies_sale(article, amount, person, added_by): """ Register a sale in the cookie corner with the free cookie discount. @@ -233,13 +263,13 @@ def free_cookies_sale(article, amount, person, added_by): discount = Discount(amount=discount_amount, date=now, discount_period=discount_period) discount.save() - transaction = CookieCornerTransaction(date=now, price=total_price, person=person, description=description, + tr = CookieCornerTransaction(date=now, price=total_price, person=person, description=description, discount=discount, article=article, amount=amount, added_by=added_by) - transaction.save() - return transaction + tr.save() + return tr -def get_transaction_payment_status(transaction: Optional[Transaction], mandate_type: Optional[str] = None) -> Tuple[str, Optional[Union[PersonalTabSettlement, ReversalTransaction, Authorization]]]: +def get_transaction_payment_status(_transaction: Optional[Transaction], mandate_type: Optional[str] = None) -> Tuple[str, Optional[Union[PersonalTabSettlement, ReversalTransaction, Authorization]]]: """ Returns information on how the given transaction was paid, or how it might probably be paid in the future. @@ -252,25 +282,25 @@ def get_transaction_payment_status(transaction: Optional[Transaction], mandate_t - If the person has no mandate, it returns ('unpaid', None). """ # Short-circuit if no transaction was given - if not transaction: + if not _transaction: return 'no_transaction', None # No transaction! # If the participation is paid, returns the settlement in which it was paid. - if transaction.is_paid(): - settlement_type = transaction.settlement.settlement_type - if settlement_type == "INSTR" and hasattr(transaction.settlement, 'debtcollectioninstruction'): - return 'paid', transaction.settlement.debtcollectioninstruction - elif settlement_type == "MANUAL" and hasattr(transaction.settlement, 'manualpaymentsettlement'): - return 'paid', transaction.settlement.manualpaymentsettlement - return 'paid', transaction.settlement + if _transaction.is_paid(): + settlement_type = _transaction.settlement.settlement_type + if settlement_type == "INSTR" and hasattr(_transaction.settlement, 'debtcollectioninstruction'): + return 'paid', _transaction.settlement.debtcollectioninstruction + elif settlement_type == "MANUAL" and hasattr(_transaction.settlement, 'manualpaymentsettlement'): + return 'paid', _transaction.settlement.manualpaymentsettlement + return 'paid', _transaction.settlement # If the participation is not paid: else: # If the transaction was reversed, it returns the ReversalTransaction. - if transaction.settlement and transaction.settlement.settlement_type == "INSTR" and hasattr(transaction.settlement, 'debtcollectioninstruction') and hasattr(transaction.settlement.debtcollectioninstruction, 'reversal'): - return 'reversed', transaction.settlement.debtcollectioninstruction.reversal.transaction + if _transaction.settlement and _transaction.settlement.settlement_type == "INSTR" and hasattr(_transaction.settlement, 'debtcollectioninstruction') and hasattr(_transaction.settlement.debtcollectioninstruction, 'reversal'): + return 'reversed', _transaction.settlement.debtcollectioninstruction.reversal.transaction # If the transaction was not reversed, returns the direct debit mandate if the person has one, else None. - return 'unpaid', transaction.person.has_mandate(mandate_type=mandate_type).order_by('authorization_type__id').first() # Returns None if no authorization exists. + return 'unpaid', _transaction.person.has_mandate(mandate_type=mandate_type).order_by('authorization_type__id').first() # Returns None if no authorization exists. def get_personal_tab_balances(only_members: bool = False, only_without_mandate: bool = True) -> List[Dict[str, Any]]: diff --git a/amelie/settings/generic.py b/amelie/settings/generic.py index 96ab5704..b598bb49 100644 --- a/amelie/settings/generic.py +++ b/amelie/settings/generic.py @@ -78,6 +78,8 @@ MEMBERS_EARLY_TERMINATION_PAYMENT_METHOD_ID = 10 # The ID of the personal_tab.PaymentMethod object representing a Forgiven payment (no payment needed). MEMBERS_FORGIVEN_PAYMENT_METHOD_ID = 12 +# The ID of the personal_tab.PaymentMethod object representing an Internal settlement (no payment needed). +INTERNAL_SETTLEMENT_PAYMENT_METHOD_ID = 14 # The LDAP host that is used to verify login attempts in the LDAP authentication module LDAP_HOST = 'hexia.ia.utwente.nl' From c59bde7000fab274ee74bc030f845019e9b277d2 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Thu, 13 Aug 2026 22:56:42 +0200 Subject: [PATCH 23/39] Fixes after review: - Fixed edge case where someone could get a negative enrollment when editing the maximum of a numeric enrollment option - Added general check to deny negative enrollment costs, even if it's theoretically allowed by the activity - Fixed numeric enrollment behaviour w.r.t. maximum places, free places, and giving up partial places - Fixed too strict negative check when numeric enrollment max per person was high but max for entire option was low - General improvements showing errors in enrollment options - Fix date comparison for re-creating transactions when editing activity - Fix creating participation transactions when the costs are 0 - Order transactions by inverse PK by default if date and added-on are not unique --- amelie/activities/forms.py | 51 +- amelie/activities/models.py | 2 +- .../templates/activity_enrollment_form.html | 7 + .../enrollmentoption_checkbox_answer.html | 22 +- .../enrollmentoption_foodquestion_answer.html | 23 +- .../enrollmentoption_numeric_answer.html | 21 +- .../enrollmentoption_question_answer.html | 23 +- amelie/activities/views.py | 439 ++++++++++-------- .../0024_delete_payment_delete_paymenttype.py | 2 +- amelie/personal_tab/models.py | 2 +- amelie/personal_tab/transactions.py | 22 +- 11 files changed, 375 insertions(+), 239 deletions(-) diff --git a/amelie/activities/forms.py b/amelie/activities/forms.py index 3acf98d0..65e8e4f1 100644 --- a/amelie/activities/forms.py +++ b/amelie/activities/forms.py @@ -1,4 +1,3 @@ -import logging from datetime import timedelta from decimal import Decimal from typing import OrderedDict, Optional, Tuple, List @@ -9,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.validators import FileExtensionValidator +from django.db.models import Sum from django.forms import widgets, SplitDateTimeField from django.utils import formats, timezone from django.utils.translation import gettext_lazy as _l @@ -121,6 +121,35 @@ class Meta: model = EnrollmentoptionNumeric fields = ["title", "price_extra", "maximum", "maximum_per_person"] + def clean_maximum(self): + data = self.cleaned_data['maximum'] + # Check if the sum of current enrollment answers does not exceed the (potentially updated) maximum + if self.instance.pk: + current_enrollment_count = self.instance.enrollmentoptionanswer_set.aggregate( + Sum('enrollmentoptionnumericanswer__answer') + ).get('enrollmentoptionnumericanswer__answer__sum') + if current_enrollment_count is not None and current_enrollment_count > data: + self.add_error( + 'maximum', + _l("The amount of current enrollments for this option would exceed the new maximum amount. " + "Please unenroll or edit the affected people first, or modify the maximum for this option.") + ) + return data + + def clean_maximum_per_person(self): + data = self.cleaned_data['maximum_per_person'] + # Check if none of the current enrollment answers exceed the (potentially updated) maximum per person. + if self.instance.pk: + for answer in self.instance.enrollmentoptionanswer_set.all(): + if answer.enrollmentoptionnumericanswer.answer > data: + self.add_error( + 'maximum_per_person', + _l("The selected amount of (at least) one of the current enrollments for this option exceeds " + "the new maximum amount per person. Please unenroll or edit the affected people first, " + "or modify the maximum for this option.") + ) + return data + class EnrollmentoptionFoodForm(forms.ModelForm): def __init__(self, *args, **kwargs): @@ -196,10 +225,27 @@ def clean_answer(self): max_per_person = self.enrollmentoption.maximum_per_person if self.enrollmentoption.maximum_per_person > 0 else None if not data: data = 0 + if data < 0: + self.add_error('answer', _l("Sorry, but the amount must be a positive number!")) if max_per_person is not None and data > max_per_person: self.add_error('answer', _l("Sorry, but you cannot exceed the maximum amount per person!")) - if data > 0 and not self.enrollmentoption.spots_left(): + + # Check the number of spots left, taking into account any spots that this enrollment might have already had + # before (in case the enrollment is being edited, which is when the self.instance.pk is set) + previous_spot_count = 0 + if self.instance.pk: + previous_spot_count = self.instance.answer + + num_spots_left = self.enrollmentoption.count_spots_left() + if num_spots_left is not None: + # Adding the previous spots to the available spots makes sure the person can reduce their + # amount without having to completely clear and re-claim them. + num_spots_left += previous_spot_count + if data > 0 and num_spots_left == 0: + self.add_error('answer', _l("Sorry, but there are no spots left!")) + elif data > 0 and (num_spots_left is not None and num_spots_left < data): self.add_error('answer', _l("Sorry, but there are not enough spots left!")) + return data class Meta: @@ -299,7 +345,6 @@ def get_form_args_kwargs(self, key, args, kwargs): # Extra per-form kwargs are stored in the initial values for the forms, which also includes a nested 'initial' key for the actual initial values. fkwargs['initial'] = None fkwargs.update(**self.initials.get(key, {})) - logging.error(f"Form {key} - args {args}, fkwargs {fkwargs}") # TODO REMOVE return args, fkwargs def get_costs(self) -> Tuple[Decimal, Decimal, bool]: diff --git a/amelie/activities/models.py b/amelie/activities/models.py index b23d9f03..2add8f4b 100644 --- a/amelie/activities/models.py +++ b/amelie/activities/models.py @@ -499,7 +499,7 @@ def has_extra_costs(self): def get_min_max_extra_costs(self) -> Tuple[Decimal, Decimal]: if self.has_extra_costs(): # 9999 is a highball estimate that should not be reached in normal use, because 'infinite' is hard to work with. - max_choice_amount = (self.maximum_per_person or self.maximum or 9999) + max_choice_amount = min(self.maximum_per_person or 9999, self.maximum or 9999, 9999) largest_extra_price = self.price_extra * max_choice_amount return min(largest_extra_price, Decimal('0.00')), max(largest_extra_price, Decimal('0.00')) else: diff --git a/amelie/activities/templates/activity_enrollment_form.html b/amelie/activities/templates/activity_enrollment_form.html index 9fd84290..ca936def 100644 --- a/amelie/activities/templates/activity_enrollment_form.html +++ b/amelie/activities/templates/activity_enrollment_form.html @@ -106,6 +106,13 @@

    {% trans 'Enrollment options' %}

    {% trans 'Warning: some of these options may cost extra!' %}

    + + {% if form.non_field_errors %} +

    + {{ form.non_field_errors }} +

    + {% endif %} + {% for form_id, subform in form.forms.items %} {% if subform.enrollmentoption_type == 'EnrollmentoptionQuestion' %} {% include "forms/enrollmentoption_question_answer.html" with form=subform %} diff --git a/amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html b/amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html index 12c452f8..33a15547 100644 --- a/amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html +++ b/amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html @@ -1,19 +1,23 @@ {% load i18n %} -

    - +

    + + +

    + {{ form.enrollmentoption.title }} +

    + + {% if form.non_field_errors %} {{ form.non_field_errors }} {% endif %} + {% if form.answer.errors %} +

    {{ form.answer.errors }}

    + {% endif %} - -

    - {{ form.enrollmentoption.title }} -

    - - + {{ form.answer }} {% trans 'Yes' %} @@ -33,4 +37,4 @@

    {% for field in form.hidden_fields %} {{ field }} {% endfor %} -

    +

    diff --git a/amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html b/amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html index 698b0990..d1888623 100644 --- a/amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html +++ b/amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html @@ -1,15 +1,8 @@ {% load i18n %} -

    - - {% if form.non_field_errors %} - - {{ form.non_field_errors }} - - {% endif %} +

    - {{ form.dishprice.errors }}

    {{ form.enrollmentoption.title }} @@ -18,7 +11,17 @@

    {% endif %}

    - + + {% if form.non_field_errors %} + + {{ form.non_field_errors }} + + {% endif %} + {% if form.dishprice.errors %} +

    {{ form.dishprice.errors }}

    + {% endif %} + +
    403 or -32096 or -32603403 or -32096 or -32098 NotLoggedInError / AuthenticationError Not logged in / The token was not recognized.
    + {% for enrollment in dogroup.list %} + + + + {% empty %} + + {% endfor %} +
    + {{ enrollment.member.incomplete_name }} +
    {% trans 'No members' %}
    + + + + {% empty %} +
    +
    +

    {% trans 'No enrollments' %}

    +
    +

    + {% trans 'There have not been any enrollments this year yet.' %} +

    +
    +
    +
    + {% endfor %} +{% endblock content %} diff --git a/amelie/members/views.py b/amelie/members/views.py index 29ca2a07..2b86e601 100644 --- a/amelie/members/views.py +++ b/amelie/members/views.py @@ -15,7 +15,7 @@ from django.views import View from django.contrib import messages from django.core.exceptions import PermissionDenied, ValidationError, BadRequest, ImproperlyConfigured -from django.db.models import Q, Sum, Max, F +from django.db.models import Q, Sum, Max, F, Count from django.utils.dateparse import parse_date from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView @@ -1067,6 +1067,26 @@ class PreRegistrationCompleteView(TemplateView): template_name = "person_registration_form_preregister_complete.html" +class RegistrationStatus(RequireCommitteeMixin, TemplateView): + template_name = "registration_status.html" + abbreviation = settings.ROOM_DUTY_ABBREVIATION + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.error_msg = None + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + memberships = Membership.objects.filter( + year=current_association_year() + ).annotate(membership_count=Count('member__membership')).filter( + membership_count=1 + ) + memberships = sorted(memberships, key=lambda x: str(x.member.dogroup) + " " + str(x.member.sortable_name())) + context['memberships'] = memberships + return context + + class PreRegistrationStatus(RequireCommitteeMixin, TemplateView): template_name = "preregistration_status.html" abbreviation = settings.ROOM_DUTY_ABBREVIATION From 593321a5ea31dd1393d8aeeb3bb87b70707386a2 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 17 Aug 2026 14:55:41 +0200 Subject: [PATCH 37/39] Update translation files --- locale/nl/LC_MESSAGES/django.po | 5457 +++++++++++++++++++++---------- 1 file changed, 3650 insertions(+), 1807 deletions(-) diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 8b33906f..bf8edfa7 100644 --- a/locale/nl/LC_MESSAGES/django.po +++ b/locale/nl/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-08 15:25+0200\n" +"POT-Creation-Date: 2026-08-17 14:55+0200\n" "PO-Revision-Date: 2026-08-08 13:42+0000\n" "Last-Translator: Kevin Alberts \n" "Language-Team: Dutch \n" @@ -27,12 +27,12 @@ msgstr "Slug van de pagina" msgid "Page content" msgstr "Inhoud van de pagina" -#: amelie/about/models.py:9 amelie/activities/models.py:25 -#: amelie/activities/models.py:481 -#: amelie/activities/templates/activity_enrollment_overview.html:79 -#: amelie/activities/templates/activity_enrollment_overview.html:83 -#: amelie/activities/templates/activity_enrollment_overview.html:231 -#: amelie/activities/templates/activity_enrollment_print.html:29 +#: amelie/about/models.py:9 amelie/activities/models.py:32 +#: amelie/activities/models.py:681 +#: amelie/activities/templates/activity_enrollment_print.html:35 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:131 +#: amelie/activities/templates/includes/activity_participants_table.html:11 +#: amelie/activities/templates/includes/activity_participants_table.html:15 #: amelie/claudia/templates/claudia/aliasgroup_list.html:21 #: amelie/claudia/templates/claudia/contact_list.html:20 #: amelie/claudia/templates/claudia/email_detail.html:16 @@ -56,11 +56,11 @@ msgstr "Inhoud van de pagina" #: amelie/claudia/templates/claudia/timeline_list.html:20 #: amelie/education/models.py:26 amelie/education/models.py:48 #: amelie/education/models.py:133 amelie/education/models.py:179 -#: amelie/members/models.py:44 amelie/members/models.py:65 -#: amelie/members/models.py:89 amelie/members/models.py:129 -#: amelie/members/models.py:226 amelie/members/models.py:256 -#: amelie/members/models.py:818 amelie/members/models.py:848 -#: amelie/members/models.py:1095 amelie/members/models.py:1118 +#: amelie/members/models.py:46 amelie/members/models.py:67 +#: amelie/members/models.py:91 amelie/members/models.py:131 +#: amelie/members/models.py:228 amelie/members/models.py:258 +#: amelie/members/models.py:845 amelie/members/models.py:1173 +#: amelie/members/models.py:1196 #: amelie/members/templates/committee_members.html:7 #: amelie/members/templates/includes/registration/personal_details.html:14 #: amelie/members/templates/includes/registration/personal_details_employee.html:14 @@ -68,20 +68,20 @@ msgstr "Inhoud van de pagina" #: amelie/members/templates/person_membership_sign.html:30 #: amelie/members/templates/preregistration_resend_confirm.html:29 #: amelie/members/templates/registration_check.html:34 -#: amelie/personal_tab/models.py:263 amelie/personal_tab/models.py:291 -#: amelie/personal_tab/models.py:328 amelie/personal_tab/statistics.py:266 +#: amelie/personal_tab/models.py:299 amelie/personal_tab/models.py:327 +#: amelie/personal_tab/models.py:364 amelie/personal_tab/statistics.py:270 #: amelie/personal_tab/templates/declaration/declaration_form.html:54 #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:15 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:22 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:31 #: amelie/room_duty/models.py:17 amelie/tools/pdf.py:103 #: amelie/tools/pdf.py:546 templates/health_check/partial_celery_tables.html:74 msgid "Name" msgstr "Naam" -#: amelie/about/models.py:10 amelie/activities/models.py:24 +#: amelie/about/models.py:10 amelie/activities/models.py:31 #: amelie/education/models.py:27 amelie/education/models.py:49 -#: amelie/members/models.py:90 amelie/members/models.py:819 -#: amelie/personal_tab/models.py:292 amelie/personal_tab/models.py:329 +#: amelie/members/models.py:92 amelie/members/models.py:846 +#: amelie/personal_tab/models.py:328 amelie/personal_tab/models.py:365 msgid "Name (en)" msgstr "Naam (en)" @@ -185,160 +185,202 @@ msgstr "IA Activiteiten" msgid "Inter-Actief's activities in the coming weeks" msgstr "De activiteiten van I.C.T.S.V. Inter-Actief in de komende tijd" -#: amelie/activities/forms.py:29 -#: amelie/activities/templates/activity_enrollment_overview.html:243 -#: amelie/activities/templates/activity_enrollment_print.html:36 -#: amelie/calendar/models.py:34 -msgid "Method of payment" -msgstr "Betaalwijze" - -#: amelie/activities/forms.py:42 -msgid "Place on bottom of waiting list" -msgstr "Plaats onderaan de wachtlijst" - -#: amelie/activities/forms.py:43 -msgid "Waiting list action" -msgstr "Wachtlijstactie" - -#: amelie/activities/forms.py:46 -msgid "Skip waiting list and enroll immediately" -msgstr "Sla de wachtlijst over en schrijf direct in" - -#: amelie/activities/forms.py:52 amelie/activities/models.py:482 -#: amelie/activities/templates/activity_enrollment_overview.html:183 -#: amelie/activities/templates/activity_enrollment_print.html:69 -#: amelie/members/models.py:821 amelie/members/models.py:1078 -#: amelie/personal_tab/forms.py:51 amelie/personal_tab/models.py:132 -#: amelie/personal_tab/models.py:295 -#: amelie/personal_tab/templates/authorization/view.html:139 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:70 +#: amelie/activities/forms.py:29 amelie/activities/models.py:682 +#: amelie/activities/templates/activity_enrollment_print.html:86 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:83 +#: amelie/members/models.py:848 amelie/personal_tab/forms.py:50 +#: amelie/personal_tab/models.py:137 amelie/personal_tab/models.py:331 +#: amelie/personal_tab/templates/authorization/view.html:144 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:53 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:44 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:93 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:18 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:27 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:124 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:75 #: amelie/personal_tab/templates/debt_collection/person_instructions.html:17 #: amelie/personal_tab/templates/debt_collection/process_batch.html:52 #: amelie/personal_tab/templates/debt_collection/view.html:95 -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:19 -#: amelie/personal_tab/templates/info/transaction_information.html:11 -#: amelie/personal_tab/templates/lists/activity_transactions.html:14 -#: amelie/personal_tab/templates/lists/alexia_transactions.html:12 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:14 -#: amelie/personal_tab/templates/lists/custom_transactions.html:12 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:12 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:11 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:12 -#: amelie/personal_tab/templates/lists/transactions.html:12 +#: amelie/personal_tab/templates/info/payments.html:23 +#: amelie/personal_tab/templates/info/payments.html:80 +#: amelie/personal_tab/templates/info/transaction_information.html:15 +#: amelie/personal_tab/templates/lists/activity_transactions.html:15 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:13 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:13 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:15 +#: amelie/personal_tab/templates/lists/custom_transactions.html:13 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:13 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:12 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:13 +#: amelie/personal_tab/templates/lists/payment_transactions.html:15 +#: amelie/personal_tab/templates/lists/transactions.html:13 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:13 #: amelie/personal_tab/templates/statistics/statistics.html:189 #: amelie/personal_tab/templates/statistics/statistics.html:244 #: amelie/personal_tab/templates/statistics/statistics.html:287 #: amelie/personal_tab/templates/statistics/statistics.html:323 -#: amelie/personal_tab/templates/statistics/statistics.html:377 -#: amelie/personal_tab/templates/statistics/statistics.html:416 -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:32 +#: amelie/personal_tab/templates/statistics/statistics.html:375 +#: amelie/personal_tab/templates/statistics/statistics.html:414 +#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:28 #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:18 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:26 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:35 #: amelie/personal_tab/templates/wrapped/wrapped.html:95 #: amelie/personal_tab/templates/wrapped/wrapped.html:195 #: amelie/personal_tab/templates/wrapped/wrapped.html:241 msgid "Price" msgstr "Prijs" -#: amelie/activities/forms.py:52 amelie/activities/models.py:67 -#: amelie/activities/models.py:280 amelie/activities/models.py:306 +#: amelie/activities/forms.py:29 amelie/activities/models.py:76 +#: amelie/activities/models.py:459 amelie/activities/models.py:491 msgid "Enrolled participants will be emailed when you change the price." msgstr "Ingeschreven deelnemers zullen een e-mail ontvangen als je de prijs aanpast." -#: amelie/activities/forms.py:97 -#, python-brace-format -msgid "Website enrolment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." +#: amelie/activities/forms.py:74 +#, fuzzy +#| msgid "Website enrolment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." +msgid "Website enrolment must have a positive price. Either turn off the ability to enrol or increase the cost of the activity." +msgstr "Inschrijving via de website heeft een maximum van {0} euro. Of schakel de inschrijfoptie uit, of verlaag de prijs van de activiteit!" + +#: amelie/activities/forms.py:77 +#, fuzzy, python-brace-format +#| msgid "Website enrolment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." +msgid "Website enrollment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." msgstr "Inschrijving via de website heeft een maximum van {0} euro. Of schakel de inschrijfoptie uit, of verlaag de prijs van de activiteit!" -#: amelie/activities/forms.py:101 amelie/activities/models.py:113 +#: amelie/activities/forms.py:81 amelie/activities/models.py:103 msgid "There is no enrollment start date entered." msgstr "Er is geen inschrijfstartdatum opgegeven." -#: amelie/activities/forms.py:104 amelie/activities/models.py:116 +#: amelie/activities/forms.py:84 amelie/activities/models.py:106 msgid "There is no enrollment end date entered." msgstr "Er is geen inschrijfstopdatum opgegeven." -#: amelie/activities/forms.py:108 +#: amelie/activities/forms.py:88 msgid "Signup end must be after the signup start." msgstr "Einde van de inschrijving valt voor het begin." -#: amelie/activities/forms.py:189 +#: amelie/activities/forms.py:99 +msgid "Place on bottom of waiting list" +msgstr "Plaats onderaan de wachtlijst" + +#: amelie/activities/forms.py:100 +msgid "Waiting list action" +msgstr "Wachtlijstactie" + +#: amelie/activities/forms.py:103 +msgid "Skip waiting list and enroll immediately" +msgstr "Sla de wachtlijst over en schrijf direct in" + +#: amelie/activities/forms.py:134 +msgid "The amount of current enrollments for this option would exceed the new maximum amount. Please unenroll or edit the affected people first, or modify the maximum for this option." +msgstr "" + +#: amelie/activities/forms.py:147 +msgid "The selected amount of (at least) one of the current enrollments for this option exceeds the new maximum amount per person. Please unenroll or edit the affected people first, or modify the maximum for this option." +msgstr "" + +#: amelie/activities/forms.py:207 msgid "Sorry, but the last spot has been claimed already!" msgstr "Sorry, maar de laatste plek is al vergeven!" -#: amelie/activities/forms.py:211 +#: amelie/activities/forms.py:229 +#, fuzzy +#| msgid "The amount must be a positive number." +msgid "Sorry, but the amount must be a positive number!" +msgstr "Het bedrag moet positief zijn." + +#: amelie/activities/forms.py:231 msgid "Sorry, but you cannot exceed the maximum amount per person!" msgstr "Sorry, maar je kunt niet over het aantal stuks per persoon!" -#: amelie/activities/forms.py:213 +#: amelie/activities/forms.py:245 +#, fuzzy +#| msgid "Sorry, but there are not enough spots left!" +msgid "Sorry, but there are no spots left!" +msgstr "Sorry, maar er zijn niet genoeg plekken over!" + +#: amelie/activities/forms.py:247 msgid "Sorry, but there are not enough spots left!" msgstr "Sorry, maar er zijn niet genoeg plekken over!" -#: amelie/activities/forms.py:236 +#: amelie/activities/forms.py:361 amelie/members/forms.py:306 +#: amelie/personal_tab/forms.py:132 amelie/personal_tab/models.py:1043 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:43 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:17 +#: amelie/personal_tab/templates/info/payments.html:79 +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:29 +#: amelie/personal_tab/templates/lists/payment_transactions.html:13 +#, fuzzy +#| msgid "Payment Method" +msgid "Payment method" +msgstr "Betalingsmethode" + +#: amelie/activities/forms.py:375 msgid "Photographer" msgstr "Fotograaf" -#: amelie/activities/forms.py:237 amelie/members/models.py:310 -#: amelie/members/models.py:945 amelie/members/models.py:1319 +#: amelie/activities/forms.py:376 amelie/members/models.py:312 +#: amelie/members/models.py:956 amelie/members/models.py:1397 #: amelie/members/templates/members/datablock_html.mail:4 #: amelie/members/templates/members/datablock_plain.mail:3 msgid "First name" msgstr "Voornaam" -#: amelie/activities/forms.py:238 amelie/activities/forms.py:240 -#: amelie/activities/forms.py:242 +#: amelie/activities/forms.py:377 amelie/activities/forms.py:379 +#: amelie/activities/forms.py:381 msgid "Use this field if a non-member is the owner of these pictures" msgstr "Gebruik dit veld als een extern iemand deze foto's heeft gemaakt." -#: amelie/activities/forms.py:239 amelie/members/models.py:311 -#: amelie/members/models.py:946 amelie/members/models.py:1320 +#: amelie/activities/forms.py:378 amelie/members/models.py:313 +#: amelie/members/models.py:957 amelie/members/models.py:1398 #: amelie/members/templates/members/datablock_html.mail:5 #: amelie/members/templates/members/datablock_plain.mail:4 msgid "Last name pre-fix" msgstr "Tussenvoegsels" -#: amelie/activities/forms.py:241 amelie/members/models.py:312 -#: amelie/members/models.py:947 amelie/members/models.py:1321 +#: amelie/activities/forms.py:380 amelie/members/models.py:314 +#: amelie/members/models.py:958 amelie/members/models.py:1399 #: amelie/members/templates/members/datablock_html.mail:6 #: amelie/members/templates/members/datablock_plain.mail:5 msgid "Last name" msgstr "Achternaam" -#: amelie/activities/forms.py:243 amelie/activities/models.py:82 -#: amelie/activities/models.py:83 +#: amelie/activities/forms.py:382 amelie/activities/models.py:92 +#: amelie/activities/models.py:93 #: amelie/activities/templates/activity_list.html:4 #: amelie/activities/templates/activity_list.html:35 #: amelie/activities/templates/activity_list.html:115 #: amelie/activities/templates/activity_old.html:4 #: amelie/activities/templates/activity_old.html:34 -#: amelie/calendar/models.py:31 amelie/calendar/models.py:133 -#: amelie/calendar/models.py:134 +#: amelie/calendar/models.py:31 amelie/calendar/models.py:209 +#: amelie/calendar/models.py:210 #: amelie/companies/templates/companies/company_event_list.html:4 #: amelie/companies/templates/companies/company_event_old.html:34 -#: amelie/personal_tab/models.py:189 amelie/personal_tab/statistics.py:285 +#: amelie/personal_tab/models.py:225 amelie/personal_tab/statistics.py:289 #: amelie/personal_tab/templates/cookie_corner_transactions.html:42 -#: amelie/personal_tab/templates/lists/activity_transactions.html:11 -#: amelie/personal_tab/templates/lists/transactions_totals.html:22 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:64 +#: amelie/personal_tab/templates/info/transaction_information.html:62 +#: amelie/personal_tab/templates/lists/activity_transactions.html:12 +#: amelie/personal_tab/templates/lists/transactions_totals.html:23 #: amelie/personal_tab/templates/statistics/statistics.html:178 #: amelie/personal_tab/templates/statistics/statistics.html:186 -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:14 #: amelie/weekmail/templates/weekmail/weekmail_mail_plain.mail:16 #: templates/basis.html:187 templates/basis.html:274 msgid "Activities" msgstr "Activiteiten" -#: amelie/activities/forms.py:244 amelie/calendar/models.py:116 +#: amelie/activities/forms.py:383 amelie/calendar/models.py:192 #: amelie/education/models.py:258 amelie/education/models.py:286 #: amelie/education/templates/education_event_overview.html:18 #: amelie/oauth/forms.py:11 amelie/videos/models.py:20 msgid "Public" msgstr "Publiek" -#: amelie/activities/forms.py:245 +#: amelie/activities/forms.py:384 msgid "Photos are also available without logging in" msgstr "Foto's zijn ook beschikbaar zonder in te loggen" -#: amelie/activities/forms.py:246 +#: amelie/activities/forms.py:385 #: amelie/activities/templates/activity_list.html:116 #: amelie/activities/templates/activity_old.html:35 #: amelie/activities/templates/gallery.html:4 @@ -356,152 +398,218 @@ msgstr "Foto's zijn ook beschikbaar zonder in te loggen" msgid "Photos" msgstr "Foto's" -#: amelie/activities/forms.py:261 +#: amelie/activities/forms.py:400 msgid "You must specify a photographer!" msgstr "Je moet een fotograaf opgeven!" -#: amelie/activities/forms.py:264 +#: amelie/activities/forms.py:403 msgid "You must specify a last name!" msgstr "Je moet een achternaam opgeven!" -#: amelie/activities/forms.py:267 +#: amelie/activities/forms.py:406 msgid "You must specify a first name!" msgstr "Je moet een voornaam opgeven!" -#: amelie/activities/forms.py:284 +#: amelie/activities/forms.py:423 #: amelie/activities/templates/activities/eventdesk/message_list.html:70 #: amelie/activities/templates/activities/eventdesk/message_list.html:102 msgid "Activity" msgstr "Activiteit" -#: amelie/activities/graphql.py:16 +#: amelie/activities/graphql.py:15 msgid "Has photos" msgstr "Heeft fotos" -#: amelie/activities/graphql.py:81 +#: amelie/activities/graphql.py:80 msgid "The absolute URL to an activity." msgstr "De absolute URL naar een activiteit." -#: amelie/activities/graphql.py:82 +#: amelie/activities/graphql.py:81 msgid "A URL to a random picture that was made at this activity." msgstr "Een URL naar een willekeurige foto die gemaakt is bij deze activiteit." -#: amelie/activities/graphql.py:83 +#: amelie/activities/graphql.py:82 msgid "A URL that points to the picture gallery for this activity." msgstr "Een URL die verwijst naar de fotogalerij van deze activiteit." -#: amelie/activities/graphql.py:84 +#: amelie/activities/graphql.py:83 msgid "A link to the ICS file for this activity." msgstr "Een link naar het ICS-bestand voor deze activiteit." -#: amelie/activities/graphql.py:85 +#: amelie/activities/graphql.py:84 msgid "Whether people can still enroll for this activity." msgstr "Of mensen zich nog kunnen inschrijven voor deze activiteit." -#: amelie/activities/graphql.py:86 +#: amelie/activities/graphql.py:85 msgid "Whether people can no longer enroll for this activity." msgstr "Of mensen zich niet meer kunnen inschrijven voor deze activiteit." -#: amelie/activities/graphql.py:87 +#: amelie/activities/graphql.py:86 msgid "Whether the person that is currently signed-in can edit this activity." msgstr "Of de huidig ingelogde persoon deze activiteit mag aanpassen." -#: amelie/activities/graphql.py:88 +#: amelie/activities/graphql.py:87 msgid "Whether this activity is full." msgstr "Of deze activiteit vol zit." -#: amelie/activities/graphql.py:89 +#: amelie/activities/graphql.py:88 msgid "Whether this activity is almost full (<= 10 places left)." msgstr "Of deze activiteit bijna vol zit (<= 10 plaatsen over)." -#: amelie/activities/graphql.py:90 +#: amelie/activities/graphql.py:89 msgid "If there are any options for enrollments." msgstr "Of er inschrijfopties zijn." -#: amelie/activities/graphql.py:91 +#: amelie/activities/graphql.py:90 msgid "If there are any costs associated with this activity." msgstr "Of er kosten zijn verbonden aan deze activiteit." -#: amelie/activities/graphql.py:158 +#: amelie/activities/graphql.py:157 msgid "Label name" msgstr "Labelnaam" -#: amelie/activities/graphql.py:159 +#: amelie/activities/graphql.py:158 msgid "Label explanation" msgstr "Labeluitleg" -#: amelie/activities/mail.py:64 amelie/activities/mail.py:66 -#: amelie/activities/mail.py:169 amelie/activities/mail.py:171 +#: amelie/activities/mail.py:63 amelie/activities/mail.py:65 +#: amelie/activities/mail.py:170 amelie/activities/mail.py:172 msgid "Inter-Activity" msgstr "Inter-Activiteit" -#: amelie/activities/models.py:26 +#: amelie/activities/models.py:33 msgid "Hexcolor without #" msgstr "Hex kleurcode (zonder #)" -#: amelie/activities/models.py:27 +#: amelie/activities/models.py:34 msgid "Icon from known icons" msgstr "Icoon uit bestaande iconen" -#: amelie/activities/models.py:28 +#: amelie/activities/models.py:35 msgid "Explanation on icon (en)" msgstr "Uitleg over icoon (en)" -#: amelie/activities/models.py:29 +#: amelie/activities/models.py:36 msgid "Explanation on icon" msgstr "Uitleg over icoon" -#: amelie/activities/models.py:30 +#: amelie/activities/models.py:37 #: amelie/companies/templates/companies/company_statistics.html:75 #: amelie/companies/templates/companies/company_statistics.html:105 msgid "Active?" msgstr "Actief?" -#: amelie/activities/models.py:57 +#: amelie/activities/models.py:64 msgid "enrollment" msgstr "inschrijving" -#: amelie/activities/models.py:58 +#: amelie/activities/models.py:65 msgid "enrollment start" msgstr "inschrijving start" -#: amelie/activities/models.py:58 +#: amelie/activities/models.py:65 msgid "If you want to add options, make sure your activity isn't open for enrollment right away" msgstr "Als je nog opties wil toevoegen, zorg ervoor dat de inschrijving dan niet meteen open is" -#: amelie/activities/models.py:59 +#: amelie/activities/models.py:66 msgid "enrollment end" msgstr "inschrijving stop" -#: amelie/activities/models.py:60 +#: amelie/activities/models.py:67 msgid "Hide participants" msgstr "Deelnemers verbergen" -#: amelie/activities/models.py:62 +#: amelie/activities/models.py:69 msgid "Lock waiting list" msgstr "Vergrendel wachtlijst" -#: amelie/activities/models.py:67 +#: amelie/activities/models.py:76 msgid "price" msgstr "prijs" -#: amelie/activities/models.py:68 +#: amelie/activities/models.py:78 msgid "can unenroll" msgstr "kan uitschrijven" -#: amelie/activities/models.py:70 +#: amelie/activities/models.py:80 msgid "icon" msgstr "icoon" -#: amelie/activities/models.py:70 +#: amelie/activities/models.py:80 msgid "Image of 175 by 275 pixels." msgstr "Afbeelding van 175 bij 275 pixels." -#: amelie/activities/models.py:72 +#: amelie/activities/models.py:82 msgid "Facebook event id, numerical value in the facebook event url." msgstr "Facebook event id, numerieke waarde in de facebook event url." -#: amelie/activities/models.py:219 +#: amelie/activities/models.py:243 +#, fuzzy, python-brace-format +#| msgid "Unenrollment for {activity} is not possible." +msgid "Enrollment for {activity} is not possible." +msgstr "Uitschrijven voor {activity} is niet mogelijk." + +#: amelie/activities/models.py:247 +#, python-brace-format +msgid "Enrollment for {activity} is not possible. You do not have an active membership at Inter-Actief" +msgstr "Inschrijven voor {activity} is niet mogelijk. Je hebt geen actief lidmaatschap bij Inter-Actief." + +#: amelie/activities/models.py:252 +#, python-brace-format +msgid "Enrollment for {activity} is only possible on the website of this activity." +msgstr "Inschrijven voor {activity} is alleen mogelijk op de website van deze activiteit." + +#: amelie/activities/models.py:258 +#, python-brace-format +msgid "The enrollment period for {activity} hasn't started yet or has already passed." +msgstr "De inschrijfperiode voor {activity} is nog niet begonnen of is al voorbij." + +#: amelie/activities/models.py:263 +#, python-brace-format +msgid "{person} is already enrolled for {activity}." +msgstr "{person} is al ingeschreven voor {activity}." + +#: amelie/activities/models.py:268 +#, python-brace-format +msgid "You are already enrolled for {activity}." +msgstr "Je bent al ingeschreven voor {activity}." + +#: amelie/activities/models.py:287 amelie/activities/models.py:307 +#, python-brace-format +msgid "Unenrollment for {activity} is not possible." +msgstr "Uitschrijven voor {activity} is niet mogelijk." + +#: amelie/activities/models.py:291 +#, python-brace-format +msgid "Unenrollment for {activity} is only possible on the website of this activity." +msgstr "Uitschrijven voor {activity} is alleen mogelijk op de website van deze activiteit." + +#: amelie/activities/models.py:299 +#, python-brace-format +msgid "{person} is not enrolled for {activity}." +msgstr "{person} is niet ingeschreven voor {activity}." + +#: amelie/activities/models.py:304 +#, python-brace-format +msgid "You are not enrolled for {activity}." +msgstr "Je bent niet ingeschreven voor {activity}." + +#: amelie/activities/models.py:310 +#, python-brace-format +msgid "The unenrollment term for {activity} has already passed." +msgstr "De uitschrijfperiode voor {activity} is al voorbij." + +#: amelie/activities/models.py:350 +#, fuzzy +#| msgid "Activities iCal feed" +msgid "Activity was cancelled" +msgstr "Activiteiten iCal-feed" + +#: amelie/activities/models.py:365 +msgid "Activity has already started, it cannot be canceled any more." +msgstr "" + +#: amelie/activities/models.py:394 #: amelie/activities/templates/activities/enrollmentoption_list.html:41 #: amelie/activities/templates/activities/enrollmentoption_list.html:87 #: amelie/activities/templates/activities/enrollmentoption_list.html:135 @@ -517,65 +625,64 @@ msgstr "Facebook event id, numerieke waarde in de facebook event url." msgid "Title" msgstr "Titel" -#: amelie/activities/models.py:225 +#: amelie/activities/models.py:400 msgid "Enrollment option" msgstr "Inschrijfoptie" -#: amelie/activities/models.py:226 +#: amelie/activities/models.py:401 #: amelie/activities/templates/activities/enrollmentoption_list.html:5 -#: amelie/activities/templates/activity_enrollment_form.html:47 -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:39 +#: amelie/activities/templates/activity_enrollment_form.html:99 +#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:19 msgid "Enrollment options" msgstr "Inschrijfopties" -#: amelie/activities/models.py:267 amelie/activities/models.py:494 +#: amelie/activities/models.py:446 amelie/activities/models.py:694 #: amelie/activities/templates/activities/enrollmentoption_list.html:136 #: amelie/activities/templates/activities/enrollmentoption_list.html:178 msgid "Required" msgstr "Verplicht" -#: amelie/activities/models.py:280 amelie/activities/models.py:306 -#: amelie/activities/models.py:345 +#: amelie/activities/models.py:459 amelie/activities/models.py:491 +#: amelie/activities/models.py:546 msgid "Price extra" msgstr "Prijs extra" -#: amelie/activities/models.py:281 amelie/activities/models.py:307 +#: amelie/activities/models.py:460 amelie/activities/models.py:492 msgid "Maximum limit of selections" msgstr "Limiet van selecties" -#: amelie/activities/models.py:281 amelie/activities/models.py:307 -#: amelie/activities/models.py:308 +#: amelie/activities/models.py:460 amelie/activities/models.py:492 +#: amelie/activities/models.py:493 msgid "Set as 0 for unlimited" msgstr "Houd op 0 voor onbeperkt" -#: amelie/activities/models.py:308 +#: amelie/activities/models.py:493 msgid "Maximum per person" msgstr "Maximum per persoon" -#: amelie/activities/models.py:358 +#: amelie/activities/models.py:559 msgid "Answer to enrollment option" msgstr "Inschrijfoptieantwoord" -#: amelie/activities/models.py:359 +#: amelie/activities/models.py:560 msgid "Anwers to enrollment option" msgstr "Inschrijfoptieantwoorden" -#: amelie/activities/models.py:407 amelie/activities/models.py:423 -#: amelie/activities/models.py:441 amelie/activities/models.py:456 -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:47 +#: amelie/activities/models.py:607 amelie/activities/models.py:623 +#: amelie/activities/models.py:641 amelie/activities/models.py:656 msgid "Respons" msgstr "Antwoord" -#: amelie/activities/models.py:419 +#: amelie/activities/models.py:619 msgid "A response is required" msgstr "Antwoord is verplicht" -#: amelie/activities/models.py:428 +#: amelie/activities/models.py:628 #: amelie/activities/templates/activities/eventdesk/message_list.html:212 -#: amelie/activities/templates/activity_confirm_cancellation.html:21 +#: amelie/activities/templates/activity_confirm_cancellation.html:29 #: amelie/activities/templates/activity_confirm_delete.html:21 -#: amelie/activities/templates/activity_confirm_unenrollment.html:19 -#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:18 +#: amelie/activities/templates/activity_confirm_unenrollment.html:25 +#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:22 #: amelie/activities/templates/photo_upload_clear_confirm.html:25 #: amelie/calendar/templates/confirm_payment.html:19 #: amelie/claudia/templates/claudia/add_to_mapping_confirm.html:21 @@ -586,17 +693,17 @@ msgstr "Antwoord is verplicht" #: amelie/members/templates/members/datablock_html.mail:26 #: amelie/members/templates/preregistration_delete_confirm.html:17 #: amelie/personal_tab/templates/print/refund.html:96 -#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:31 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:42 +#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:36 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:56 #: amelie/weekmail/templates/weekmail/weekmailnewsarticle_confirm_delete.html:16 msgid "Yes" msgstr "Ja" -#: amelie/activities/models.py:428 +#: amelie/activities/models.py:628 #: amelie/activities/templates/activities/eventdesk/message_list.html:215 -#: amelie/activities/templates/activity_confirm_cancellation.html:22 +#: amelie/activities/templates/activity_confirm_cancellation.html:30 #: amelie/activities/templates/activity_confirm_delete.html:22 -#: amelie/activities/templates/activity_confirm_unenrollment.html:20 +#: amelie/activities/templates/activity_confirm_unenrollment.html:26 #: amelie/activities/templates/photo_upload_clear_confirm.html:26 #: amelie/calendar/templates/confirm_payment.html:20 #: amelie/claudia/templates/claudia/add_to_mapping_confirm.html:22 @@ -607,60 +714,63 @@ msgstr "Ja" #: amelie/members/templates/members/datablock_html.mail:26 #: amelie/members/templates/preregistration_delete_confirm.html:18 #: amelie/personal_tab/templates/print/refund.html:97 -#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:33 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:44 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:61 +#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:38 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:58 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:75 msgid "No" msgstr "Nee" -#: amelie/activities/models.py:461 amelie/activities/models.py:528 +#: amelie/activities/models.py:661 amelie/activities/models.py:728 msgid "(none)" msgstr "(geen)" -#: amelie/activities/models.py:480 amelie/activities/models.py:495 +#: amelie/activities/models.py:680 amelie/activities/models.py:695 msgid "Restaurant" msgstr "Restaurant" -#: amelie/activities/models.py:483 amelie/personal_tab/models.py:296 -#: amelie/personal_tab/models.py:330 amelie/room_duty/models.py:247 +#: amelie/activities/models.py:683 amelie/personal_tab/models.py:332 +#: amelie/personal_tab/models.py:366 amelie/room_duty/models.py:247 msgid "Available" msgstr "Beschikbaar" -#: amelie/activities/models.py:484 -#: amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html:39 +#: amelie/activities/models.py:684 +#: amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html:42 msgid "Allergens" msgstr "Allergiën" -#: amelie/activities/models.py:484 +#: amelie/activities/models.py:684 msgid "No information available" msgstr "Geen informatie beschikbaar" -#: amelie/activities/models.py:509 +#: amelie/activities/models.py:709 msgid "Enrollment optionquestion" msgstr "Inschrijfoptievraag" -#: amelie/activities/models.py:520 +#: amelie/activities/models.py:720 msgid "Dish cost" msgstr "Prijs voor gerecht" -#: amelie/activities/models.py:557 +#: amelie/activities/models.py:739 #: amelie/activities/templates/activities/eventdesk/message_list.html:51 #: amelie/personal_tab/templates/info/rfid_cards.html:15 msgid "Registered" msgstr "Aangemeld" -#: amelie/activities/models.py:558 +#: amelie/activities/models.py:740 #: amelie/activities/templates/activities/eventdesk/message_list.html:55 msgid "Accepted" msgstr "Geaccepteerd" -#: amelie/activities/models.py:559 amelie/activities/views.py:1615 -#: amelie/activities/views.py:1621 amelie/activities/views.py:1627 -#: amelie/members/models.py:295 +#: amelie/activities/models.py:741 amelie/activities/views.py:1646 +#: amelie/activities/views.py:1652 amelie/activities/views.py:1658 +#: amelie/members/models.py:297 #: amelie/members/templates/person_accounts.html:37 #: amelie/personal_tab/templates/info/rfid_cards.html:45 -#: amelie/personal_tab/templates/print/log_list.html:94 -#: amelie/personal_tab/templates/print/log_list.html:116 +#: amelie/personal_tab/templates/lists/payment_transactions.html:48 +#: amelie/personal_tab/templates/lists/payment_transactions.html:50 +#: amelie/personal_tab/templates/lists/payment_transactions.html:52 +#: amelie/personal_tab/templates/print/log_list.html:99 +#: amelie/personal_tab/templates/print/log_list.html:121 #: amelie/personal_tab/templates/print/refund.html:63 #: amelie/personal_tab/templates/print/refund.html:88 #: templates/health_check/partial_celery_tables.html:30 @@ -668,61 +778,108 @@ msgstr "Geaccepteerd" #: templates/health_check/partial_celery_tables.html:56 #: templates/health_check/partial_celery_tables.html:90 #: templates/health_check/partial_celery_tables.html:101 -#: templates/profile_overview.html:297 +#: templates/profile_overview.html:303 msgid "Unknown" msgstr "Onbekend" -#: amelie/activities/models.py:561 +#: amelie/activities/models.py:743 msgid "E-mail message ID" msgstr "E-mailbericht-ID" -#: amelie/activities/models.py:562 +#: amelie/activities/models.py:744 msgid "E-mail message timestamp" msgstr "Tijdstempel E-mailbericht" -#: amelie/activities/models.py:564 +#: amelie/activities/models.py:746 msgid "New state of the event registration" msgstr "Nieuwe status van de evenementaanmelding" -#: amelie/activities/models.py:566 +#: amelie/activities/models.py:748 msgid "Person that requested the registration as stated in the e-mail" msgstr "Persoon die de aanmelding heeft aangevraagd volgens de e-mail" -#: amelie/activities/models.py:568 +#: amelie/activities/models.py:750 msgid "Name of the event as stated in the e-mail" msgstr "Naam van het evenement volgens de e-mail" -#: amelie/activities/models.py:569 +#: amelie/activities/models.py:751 msgid "Start date/time of the event as stated in the e-mail" msgstr "Startdatum/tijd van het evenement volgens de e-mail" -#: amelie/activities/models.py:570 +#: amelie/activities/models.py:752 msgid "End date/time of the event as stated in the e-mail" msgstr "Einddatum/tijd van het evenement volgens de e-mail" -#: amelie/activities/models.py:571 +#: amelie/activities/models.py:753 msgid "Location of the event as stated in the e-mail" msgstr "Locatie van het evenement volgens de e-mail" -#: amelie/activities/models.py:572 +#: amelie/activities/models.py:754 msgid "Percentage certainty of match with activity." msgstr "Zekerheidspercentage van de match met de activiteit." -#: amelie/activities/models.py:610 +#: amelie/activities/models.py:792 msgid "n/a" msgstr "n.v.t." -#: amelie/activities/models.py:612 +#: amelie/activities/models.py:794 #, python-format, python-brace-format msgid "{0:.1f}% certain" msgstr "{0:.1f}% zeker" #: amelie/activities/templates/activities/activity_cancelled.mail:3 +#, fuzzy, python-format +#| msgid "Cancellation of " +msgid "Cancellation of %(activity)s" +msgstr "Annulering van " + +#: amelie/activities/templates/activities/activity_cancelled.mail:4 +#: amelie/claudia/templates/claudia/account_deleted.mail:5 +#, python-format +msgid "Dear %(name)s," +msgstr "Beste %(name)s," + +#: amelie/activities/templates/activities/activity_cancelled.mail:5 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| "You receive this email because you were enrolled for %(summary)s. This activity has been cancelled.\n" +msgid "" +"\n" +"You receive this email because you were enrolled for %(activity)s. This activity has been cancelled.\n" +msgstr "" +"\n" +"Je ontvangt deze e-mail omdat je bent ingeschreven voor %(summary)s. Deze activiteit is geannuleerd.\n" + +#: amelie/activities/templates/activities/activity_cancelled.mail:8 +#, fuzzy, python-format +#| msgid "The total costs are %(costs)s euro. Payment method: %(paymentmethod)s." +msgid "The total costs were %(costs)s euro." +msgstr "De kosten zijn totaal %(costs)s euro. Betaalwijze: %(paymentmethod)s." + +#: amelie/activities/templates/activities/activity_cancelled.mail:9 +msgid "These costs will be refunded on your personal tab." +msgstr "" + +#: amelie/activities/templates/activities/activity_cancelled.mail:10 +msgid "If you already paid for this activity, please come by the association room or contact the treasurer to re-collect your payment. If you had not paid yet you do not need to do anything." +msgstr "" + +#: amelie/activities/templates/activities/activity_cancelled.mail:11 +#: amelie/activities/templates/activities/activity_cancelled_from_waiting_list.mail:7 +#: amelie/activities/templates/activities/activity_cancelled_treasurer.mail:13 +#: amelie/activities/templates/activities/activity_enrolled_on_waiting_list.mail:15 +msgid "" +"\n" +"Kind regards," +msgstr "" +"\n" +"Met vriendelijke groet," + #: amelie/activities/templates/activities/activity_cancelled_from_waiting_list.mail:3 msgid "Cancellation of " msgstr "Annulering van " -#: amelie/activities/templates/activities/activity_cancelled.mail:4 #: amelie/activities/templates/activities/activity_cancelled_from_waiting_list.mail:4 #: amelie/activities/templates/activities/activity_enrolled.mail:5 #: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:5 @@ -742,36 +899,12 @@ msgstr "Annulering van " #: amelie/members/templates/members/yeartransfer_ended_eniac.mail:6 #: amelie/members/templates/members/yeartransfer_ended_final.mail:6 #: amelie/members/templates/members/yeartransfer_prolonged.mail:6 +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:6 #: amelie/tools/templates/send_oauth_link_code.mail:6 #: amelie/weekmail/templates/weekmail/weekmail_mail_plain.mail:1 msgid "Dear" msgstr "Beste" -#: amelie/activities/templates/activities/activity_cancelled.mail:5 -#, python-format -msgid "" -"\n" -"You receive this email because you were enrolled for %(summary)s. This activity has been cancelled.\n" -msgstr "" -"\n" -"Je ontvangt deze e-mail omdat je bent ingeschreven voor %(summary)s. Deze activiteit is geannuleerd.\n" - -#: amelie/activities/templates/activities/activity_cancelled.mail:8 -#, python-format -msgid "The total costs were %(participation_costs)s euro. Your payment method was %(paymentmethod)s. These costs will be refunded on your personal tab in the case of a mandate or you can collect your cash payment at the association room in the case of a cash payment." -msgstr "De totale kosten waren %(participation_costs)s euro. Jouw betaalmethode was %(paymentmethod)s. Deze kosten krijg je teruggestort op je streeplijst als je via een machtiging hebt betaald. Als je contant betaald hebt, dan kan je bij de vereningskamer je geld ophalen." - -#: amelie/activities/templates/activities/activity_cancelled.mail:9 -#: amelie/activities/templates/activities/activity_cancelled_from_waiting_list.mail:7 -#: amelie/activities/templates/activities/activity_cancelled_treasurer.mail:13 -#: amelie/activities/templates/activities/activity_enrolled_on_waiting_list.mail:15 -msgid "" -"\n" -"Kind regards," -msgstr "" -"\n" -"Met vriendelijke groet," - #: amelie/activities/templates/activities/activity_cancelled_from_waiting_list.mail:5 #, python-format msgid "" @@ -808,40 +941,51 @@ msgstr "Je ontvangt deze e-mail omdat je bent ingeschreven voor %(summary)s. Dez #: amelie/activities/templates/activities/activity_enrolled.mail:9 #: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:9 -#, python-format -msgid "The total costs are %(costs)s euro. Payment method: %(paymentmethod)s." +#, fuzzy, python-format +#| msgid "The total costs are %(costs)s euro. Payment method: %(paymentmethod)s." +msgid "The total costs are %(costs)s euro." msgstr "De kosten zijn totaal %(costs)s euro. Betaalwijze: %(paymentmethod)s." -#: amelie/activities/templates/activities/activity_enrolled.mail:11 -#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:11 +#: amelie/activities/templates/activities/activity_enrolled.mail:10 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:10 +msgid "These costs will be debited from your bank account via your direct debit mandate." +msgstr "" + +#: amelie/activities/templates/activities/activity_enrolled.mail:10 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:10 +msgid "These costs will need to be paid manually by you, either at the activity or at the board." +msgstr "" + +#: amelie/activities/templates/activities/activity_enrolled.mail:12 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:12 msgid "For this activity, the following enrollment options exist:" msgstr "Bij deze activiteit zijn de volgende inschrijfopties ingevuld:" -#: amelie/activities/templates/activities/activity_enrolled.mail:21 -#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:21 +#: amelie/activities/templates/activities/activity_enrolled.mail:22 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:22 #, python-format msgid "It's possible to edit your enrollment or unenroll until %(enrollment_end)s." msgstr "Tot %(enrollment_end)s is het mogelijk om je inschrijving te wijzigen of te annuleren." -#: amelie/activities/templates/activities/activity_enrolled.mail:23 -#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:23 +#: amelie/activities/templates/activities/activity_enrolled.mail:24 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:24 msgid "For this activity, you can only unenroll yourself by contacting the board." msgstr "Voor deze activiteit kun je jezelf alleen uitschrijven indien je contact met het bestuur opneemt." -#: amelie/activities/templates/activities/activity_enrolled.mail:25 +#: amelie/activities/templates/activities/activity_enrolled.mail:26 #: amelie/activities/templates/activities/activity_enrolled_on_waiting_list.mail:11 #, python-format msgid "You have been enrolled by %(enrolled_by)s. In case you did not approve this, get in touch with the board." msgstr "Je bent ingeschreven door %(enrolled_by)s. Indien je hier geen toestemming voor gegeven hebt, neem dan contact op met het bestuur." -#: amelie/activities/templates/activities/activity_enrolled.mail:27 -#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:25 +#: amelie/activities/templates/activities/activity_enrolled.mail:28 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:26 #: amelie/activities/templates/activities/activity_enrolled_on_waiting_list.mail:13 msgid "An invite is attached. You can import this in your agenda. In case you no longer want to receive invites through e-mail, edit your preferences on the website." msgstr "In de bijlage is een invite toegevoegd. Deze is te importeren in je agenda. Wil je geen invite meer ontvangen in de e-mail, wijzig dan je voorkeuren op de website." -#: amelie/activities/templates/activities/activity_enrolled.mail:29 -#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:27 +#: amelie/activities/templates/activities/activity_enrolled.mail:30 +#: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:28 msgid "" "See you then!\n" "\n" @@ -984,7 +1128,8 @@ msgstr "Opslaan en inschrijfopties toevoegen" #: amelie/activities/templates/activities/activity_form.html:104 #: amelie/activities/templates/activities/activity_form.html:107 #: amelie/activities/templates/activities/enrollmentoption_confirm_delete.html:21 -#: amelie/activities/templates/activity_enrollment_form.html:107 +#: amelie/activities/templates/activity_enrollment_form.html:194 +#: amelie/activities/templates/includes/activity_participation_create_payment.html:60 #: amelie/claudia/templates/accounts/mailing_alias.html:18 #: amelie/claudia/templates/claudia/extrapersonalalias_delete.html:21 #: amelie/companies/templates/companies/company_event_form.html:111 @@ -994,8 +1139,8 @@ msgstr "Opslaan en inschrijfopties toevoegen" #: amelie/members/templates/committee_edit_data.html:8 #: amelie/members/templates/committee_edit_members.html:73 #: amelie/members/templates/committee_edit_single_member.html:66 -#: amelie/members/templates/members/payment_confirm_delete.html:21 #: amelie/members/templates/person_activate_mandate.html:19 +#: amelie/members/templates/person_confirm_mandate.html:23 #: amelie/members/templates/person_delete_mandate.html:28 #: amelie/members/templates/person_edit_data.html:18 #: amelie/members/templates/person_edit_employee.html:9 @@ -1008,18 +1153,19 @@ msgstr "Opslaan en inschrijfopties toevoegen" #: amelie/members/templates/person_end_membership.html:21 #: amelie/members/templates/person_existing_mandate.html:17 #: amelie/members/templates/person_mandate_sign.html:48 +#: amelie/members/templates/person_membership_create_payment.html:60 #: amelie/members/templates/person_membership_sign.html:48 #: amelie/members/templates/person_new_mandate.html:11 #: amelie/members/templates/person_new_membership.html:11 -#: amelie/members/templates/person_payment.html:15 #: amelie/members/templates/preregistration_resend_confirm.html:42 #: amelie/personal_tab/templates/authorization/amendment_delete.html:16 #: amelie/personal_tab/templates/cookie_corner_article_form.html:38 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:16 #: amelie/personal_tab/templates/debt_collection/instruction_reversal_delete.html:16 #: amelie/personal_tab/templates/pos/register_external_card.html:40 #: amelie/personal_tab/templates/pos/scan_external_card.html:27 #: amelie/personal_tab/templates/transactions/transaction_delete.html:16 -#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:46 +#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:55 #: amelie/room_duty/templates/room_duty/balcony_duty/delete.html:17 #: amelie/room_duty/templates/room_duty/pool/delete.html:38 #: amelie/room_duty/templates/room_duty/pool/form.html:17 @@ -1035,7 +1181,7 @@ msgstr "Annuleer" #: amelie/activities/templates/activities/activity_form.html:118 #: amelie/activities/templates/activity.html:182 #: amelie/activities/templates/activity_list.html:36 -#: amelie/personal_tab/models.py:191 +#: amelie/personal_tab/models.py:227 msgid "Enrollment" msgstr "Inschrijving" @@ -1140,12 +1286,12 @@ msgstr "" #: amelie/claudia/templates/claudia/extrapersonalalias_delete.html:19 #: amelie/companies/templates/companies/company_event.html:20 #: amelie/education/templates/education_event.html:19 -#: amelie/members/templates/members/payment_confirm_delete.html:19 #: amelie/members/templates/person_delete_mandate.html:31 #: amelie/members/templates/person_edit_employee.html:13 #: amelie/members/templates/person_mandate.html:85 #: amelie/personal_tab/templates/authorization/amendment_delete.html:21 -#: amelie/personal_tab/templates/authorization/view.html:98 +#: amelie/personal_tab/templates/authorization/view.html:103 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:21 #: amelie/personal_tab/templates/debt_collection/instruction_reversal_delete.html:21 #: amelie/personal_tab/templates/info/rfid_cards.html:63 #: amelie/personal_tab/templates/transactions/transaction_delete.html:21 @@ -1166,7 +1312,7 @@ msgstr "" #: amelie/weekmail/templates/weekmail/weekmail_wizard.html:83 #: templates/oauth2_provider/authorized-token-delete.html:13 #: templates/oauth2_provider/includes/user_oauth_table.html:9 -#: templates/profile_overview.html:175 templates/profile_overview.html:186 +#: templates/profile_overview.html:181 templates/profile_overview.html:192 msgid "Delete" msgstr "Verwijder" @@ -1258,7 +1404,8 @@ msgstr "Plekken over" #: amelie/members/templates/person_mandate.html:84 #: amelie/members/templates/person_preferences.html:18 #: amelie/members/templates/person_study.html:38 -#: amelie/personal_tab/templates/authorization/view.html:97 +#: amelie/personal_tab/templates/authorization/view.html:102 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:25 #: amelie/personal_tab/templates/info/account_information.html:62 #: amelie/personal_tab/templates/transactions/transaction_form.html:32 #: amelie/room_duty/templates/room_duty/table/list.html:21 @@ -1340,22 +1487,29 @@ msgstr "Metadata" #: amelie/companies/templates/companies/company_statistics.html:137 #: amelie/education/templates/education_event_overview.html:15 #: amelie/education/templates/education_event_overview.html:65 -#: amelie/members/models.py:1076 #: amelie/members/templates/statistics/overview.html:15 -#: amelie/personal_tab/forms.py:35 amelie/personal_tab/forms.py:43 -#: amelie/personal_tab/forms.py:92 amelie/personal_tab/forms.py:109 -#: amelie/personal_tab/models.py:131 -#: amelie/personal_tab/templates/authorization/view.html:66 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:122 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:154 +#: amelie/personal_tab/forms.py:34 amelie/personal_tab/forms.py:42 +#: amelie/personal_tab/forms.py:90 amelie/personal_tab/forms.py:107 +#: amelie/personal_tab/models.py:136 +#: amelie/personal_tab/templates/authorization/view.html:71 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:40 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:91 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:15 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:122 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:127 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:159 #: amelie/personal_tab/templates/debt_collection/person_instructions.html:15 -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:17 -#: amelie/personal_tab/templates/info/transaction_information.html:3 -#: amelie/personal_tab/templates/lists/alexia_transactions.html:8 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:8 -#: amelie/personal_tab/templates/lists/custom_transactions.html:8 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:8 -#: amelie/personal_tab/templates/lists/transactions.html:8 +#: amelie/personal_tab/templates/info/payments.html:21 +#: amelie/personal_tab/templates/info/payments.html:77 +#: amelie/personal_tab/templates/info/transaction_information.html:7 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:9 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:9 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:9 +#: amelie/personal_tab/templates/lists/custom_transactions.html:9 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:9 +#: amelie/personal_tab/templates/lists/payment_transactions.html:10 +#: amelie/personal_tab/templates/lists/transactions.html:9 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:9 #: amelie/personal_tab/templates/statistics/statistics.html:184 #: amelie/personal_tab/templates/statistics/statistics.html:238 #: amelie/personal_tab/templates/statistics/statistics.html:319 @@ -1669,7 +1823,7 @@ msgstr "Begindatum/tijd" #: amelie/activities/templates/activities/eventdesk/message_list.html:103 #: amelie/data_export/templates/data_export/dataexport_detail.html:89 #: amelie/members/templates/registration_check.html:100 -#: amelie/personal_tab/forms.py:150 +#: amelie/personal_tab/forms.py:182 #: amelie/personal_tab/templates/debt_collection/process_batch.html:36 #: amelie/personal_tab/templates/debt_collection/view.html:80 #: amelie/personal_tab/templates/info/rfid_cards.html:14 @@ -1715,7 +1869,7 @@ msgstr "Start/eindtijd" #: amelie/activities/templates/activities/eventdesk/message_list.html:153 #: amelie/activities/templates/activities/eventdesk/message_list.html:194 -#: amelie/calendar/models.py:113 +#: amelie/calendar/models.py:189 msgid "Location" msgstr "Locatie" @@ -1799,7 +1953,7 @@ msgstr "Wijzig inschrijfopties" msgid "Undo event cancellation" msgstr "Maak annuleren van evenement ongedaan" -#: amelie/activities/templates/activity.html:45 amelie/calendar/models.py:124 +#: amelie/activities/templates/activity.html:45 amelie/calendar/models.py:200 msgid "Cancel event" msgstr "Annuleer evenement" @@ -1827,8 +1981,8 @@ msgid "Label" msgstr "Type" #: amelie/activities/templates/activity.html:96 -#: amelie/activities/templates/activity_enrollment_form.html:79 -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:48 +#: amelie/activities/templates/activity_enrollment_form.html:138 +#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:28 msgid "Costs" msgstr "Kosten" @@ -1872,22 +2026,51 @@ msgstr "Print inschrijvingen" msgid "Data export statistics" msgstr "Data-exportstatistieken" +#: amelie/activities/templates/activity_confirm_cancellation.html:4 +#: amelie/activities/templates/activity_confirm_cancellation.html:8 +#, fuzzy +#| msgid "Confirmation cancel" +msgid "Confirmation undo cancel" +msgstr "Bevestig verwijdering" + #: amelie/activities/templates/activity_confirm_cancellation.html:4 #: amelie/activities/templates/activity_confirm_cancellation.html:8 msgid "Confirmation cancel" msgstr "Bevestig verwijdering" -#: amelie/activities/templates/activity_confirm_cancellation.html:11 -#, python-format +#: amelie/activities/templates/activity_confirm_cancellation.html:12 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Are you sure you want to cancel %(obj)s?\n" +#| " " msgid "" "\n" -" Are you sure you want to cancel %(obj)s?\n" -" " +" Are you sure you want to undo the cancelation of %(obj)s?\n" +" " msgstr "" "\n" "Weet je zeker dat je %(obj)s wilt annuleren?" #: amelie/activities/templates/activity_confirm_cancellation.html:16 +msgid "The event will be available again, but enrollments will stay closed and need to be manually re-opened." +msgstr "" + +#: amelie/activities/templates/activity_confirm_cancellation.html:19 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Are you sure you want to cancel %(obj)s?\n" +#| " " +msgid "" +"\n" +" Are you sure you want to cancel %(obj)s?\n" +" " +msgstr "" +"\n" +"Weet je zeker dat je %(obj)s wilt annuleren?" + +#: amelie/activities/templates/activity_confirm_cancellation.html:23 msgid "All enrollments will be cancelled, the transactions will be undone, and all the participants will receive an email." msgstr "Alle inschrijvingen worden verwijderd, de transacties worden ongedaan gemaakt, en alle deelnemers krijgen hiervan een email." @@ -1924,130 +2107,347 @@ msgstr "Bevestiging uitschrijving" msgid "Confirmation unenrollment" msgstr "Bevestiging uitschrijving" -#: amelie/activities/templates/activity_confirm_unenrollment.html:11 -#, python-format +#: amelie/activities/templates/activity_confirm_unenrollment.html:12 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Are you sure you want to unenroll %(person)s for %(obj)s?\n" +#| " " msgid "" "\n" -" Are you sure you want to unenroll %(person)s for %(obj)s?\n" -" " +" Are you sure you want to unenroll %(person)s for %(obj)s?\n" +" " msgstr "" "\n" "Weet je zeker dat je %(person)s wil uitschrijven voor %(obj)s?" -#: amelie/activities/templates/activity_enrollment_form.html:5 -#: amelie/activities/templates/activity_enrollment_form.html:16 -msgid "Enroll for" -msgstr "Inschrijven voor" - -#: amelie/activities/templates/activity_enrollment_form.html:14 -msgid "enroll for" -msgstr "inschrijven voor" - -#: amelie/activities/templates/activity_enrollment_form.html:23 -#, python-format +#: amelie/activities/templates/activity_confirm_unenrollment.html:16 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Are you sure you want to unenroll %(person)s for %(obj)s?\n" +#| " " msgid "" "\n" -"\t\t\t\t\t\t \tWith this form, you will enroll %(name)s for the activity.\n" -"\t\t\t\t\t\t" +" Are you sure you want to unenroll for %(obj)s?\n" +" " msgstr "" "\n" -"Met dit formulier schrijf je %(name)s in voor de activiteit." +"Weet je zeker dat je %(person)s wil uitschrijven voor %(obj)s?" -#: amelie/activities/templates/activity_enrollment_form.html:27 +#: amelie/activities/templates/activity_enrollment_form.html:7 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Enroll person for %(name)s\n" +#| " " msgid "" "\n" -"\t\t\t\t\t\t \tClick \"Enroll\" below this form to confirm your enrollment.\n" -"\t\t\t\t\t\t" +" Update enrollment of %(person)s for %(summary)s\n" +" " msgstr "" "\n" -"Klik onderaan dit formulier op inschrijven om je inschrijving te bevestigen." - -#: amelie/activities/templates/activity_enrollment_form.html:33 -msgid "The enrollment fee is" -msgstr "De inschrijfkosten bedragen" +"Persoon inschrijven voor %(name)s" -#: amelie/activities/templates/activity_enrollment_form.html:39 -msgid "Unfortunately, there are no more openings. You will be enrolled on the waiting list." -msgstr "Helaas, maar er is geen beschikbare plek meer. Je kunt je wel inschrijven op de wachtlijst." +#: amelie/activities/templates/activity_enrollment_form.html:11 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Add credit for %(person)s.\n" +#| " " +msgid "" +"\n" +" Update enrollment for %(summary)s\n" +" " +msgstr "" +"\n" +"Tentamenkoekentegoed toevoegen voor %(person)s" -#: amelie/activities/templates/activity_enrollment_form.html:51 +#: amelie/activities/templates/activity_enrollment_form.html:17 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Enroll person for %(name)s\n" +#| " " msgid "" "\n" -" The activity has a few enrollment options. Please fill these out before you enroll.\n" -"\t\t\t\t\t\t Warning: some of these options may cost extra!\n" -" " +" Enroll %(person)s for %(summary)s\n" +" " msgstr "" "\n" -"Bij deze activiteit horen enkele inschrijfopties. Vul deze in voordat je je inschrijft.\n" -"Let op: aan sommige opties kunnen extra kosten verbonden zijn!" +"Persoon inschrijven voor %(name)s" -#: amelie/activities/templates/activity_enrollment_form.html:84 -#, python-format +#: amelie/activities/templates/activity_enrollment_form.html:21 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Enroll person for %(name)s\n" +#| " " msgid "" "\n" -" It is assumed that %(name)s has given their permission to be enrolled.\n" -" " +" Enroll for %(summary)s\n" +" " msgstr "" "\n" -"Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om zich te laten inschrijven." +"Persoon inschrijven voor %(name)s" -#: amelie/activities/templates/activity_enrollment_form.html:93 +#: amelie/activities/templates/activity_enrollment_form.html:35 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Last %(count)s transactions\n" +#| " " msgid "" "\n" -" By pressing the sign-up-button, I declare to be aware that any enrollment costs will be\n" -" taken from my bank account through direct debit by Inter-Actief in accordance with\n" -" the authorization I granted for consumptions and activities.\n" +" Update enrollment of %(person)s for %(summary)s\n" " " msgstr "" "\n" -"Door op de knop inschrijven te klikken verklaar ik dat ik op de hoogte ben van het feit dat (eventuele)\n" +"Laatste %(count)s toegevoegde transacties." + +#: amelie/activities/templates/activity_enrollment_form.html:39 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " 1 person signed up.\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(sign_ups)s people signed up.\n" +#| " " +msgid "" +"\n" +" Update enrollment for %(summary)s\n" +" " +msgstr "" +"\n" +" 1 persoon is ingeschreven.\n" +" " + +#: amelie/activities/templates/activity_enrollment_form.html:45 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Enroll person for %(name)s\n" +#| " " +msgid "" +"\n" +" Enroll %(person)s for %(summary)s\n" +" " +msgstr "" +"\n" +"Persoon inschrijven voor %(name)s" + +#: amelie/activities/templates/activity_enrollment_form.html:49 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(time)s ago\n" +#| " " +msgid "" +"\n" +" Enroll for %(summary)s\n" +" " +msgstr "" +"\n" +" %(time)s geleden\n" +" " + +#: amelie/activities/templates/activity_enrollment_form.html:60 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| "\t\t\t\t\t\t \tWith this form, you will enroll %(name)s for the activity.\n" +#| "\t\t\t\t\t\t" +msgid "" +"\n" +"\t\t\t\t\t\t \tWith this form, you will update the enrollment of %(name)s for the activity.\n" +"\t\t\t\t\t\t" +msgstr "" +"\n" +"Met dit formulier schrijf je %(name)s in voor de activiteit." + +#: amelie/activities/templates/activity_enrollment_form.html:64 +#, fuzzy +#| msgid "" +#| "\n" +#| "\t\t\t\t\t\t \tClick \"Enroll\" below this form to confirm your enrollment.\n" +#| "\t\t\t\t\t\t" +msgid "" +"\n" +"\t\t\t\t\t\t \tClick \"Update\" below this form to save the changes to your enrollment.\n" +"\t\t\t\t\t\t" +msgstr "" +"\n" +"Klik onderaan dit formulier op inschrijven om je inschrijving te bevestigen." + +#: amelie/activities/templates/activity_enrollment_form.html:70 +#, python-format +msgid "" +"\n" +"\t\t\t\t\t\t \tWith this form, you will enroll %(name)s for the activity.\n" +"\t\t\t\t\t\t" +msgstr "" +"\n" +"Met dit formulier schrijf je %(name)s in voor de activiteit." + +#: amelie/activities/templates/activity_enrollment_form.html:74 +msgid "" +"\n" +"\t\t\t\t\t\t \tClick \"Enroll\" below this form to confirm your enrollment.\n" +"\t\t\t\t\t\t" +msgstr "" +"\n" +"Klik onderaan dit formulier op inschrijven om je inschrijving te bevestigen." + +#: amelie/activities/templates/activity_enrollment_form.html:81 +msgid "The enrollment fee is" +msgstr "De inschrijfkosten bedragen" + +#: amelie/activities/templates/activity_enrollment_form.html:88 +#, fuzzy +#| msgid "Unfortunately, there are no more openings. You will be enrolled on the waiting list." +msgid "There are no more openings. The person will be enrolled on the waiting list unless you override this." +msgstr "Helaas, maar er is geen beschikbare plek meer. Je kunt je wel inschrijven op de wachtlijst." + +#: amelie/activities/templates/activity_enrollment_form.html:90 +msgid "Unfortunately, there are no more openings. You will be enrolled on the waiting list." +msgstr "Helaas, maar er is geen beschikbare plek meer. Je kunt je wel inschrijven op de wachtlijst." + +#: amelie/activities/templates/activity_enrollment_form.html:103 +msgid "The activity has a few enrollment options. Please fill these out before you enroll." +msgstr "" + +#: amelie/activities/templates/activity_enrollment_form.html:106 +msgid "Warning: some of these options may cost extra!" +msgstr "" + +#: amelie/activities/templates/activity_enrollment_form.html:144 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " It is assumed that %(name)s has given their permission to be enrolled.\n" +#| " " +msgid "" +"\n" +" It is assumed that %(name)s has given their permission for their enrollment to be modified.\n" +" " +msgstr "" +"\n" +"Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om zich te laten inschrijven." + +#: amelie/activities/templates/activity_enrollment_form.html:148 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " It is assumed that %(name)s has given their permission to be enrolled.\n" +#| " " +msgid "" +"\n" +" It is assumed that %(name)s has given their permission to be enrolled.\n" +" " +msgstr "" +"\n" +"Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om zich te laten inschrijven." + +#: amelie/activities/templates/activity_enrollment_form.html:159 +msgid "" +"\n" +" By pressing the sign-up-button, I declare to be aware that any enrollment costs will be\n" +" taken from my bank account through direct debit by Inter-Actief in accordance with\n" +" the authorization I granted for consumptions and activities.\n" +" " +msgstr "" +"\n" +"Door op de knop inschrijven te klikken verklaar ik dat ik op de hoogte ben van het feit dat (eventuele)\n" "inschrijfkosten door Inter-Actief van mijn rekening zullen worden afschreven conform de door\n" "mij getekende machtiging voor consumpties en activiteiten." -#: amelie/activities/templates/activity_enrollment_form.html:102 -#: amelie/activities/templates/activity_enrollment_overview.html:189 -#: amelie/activities/templates/activity_enrollment_print.html:73 +#: amelie/activities/templates/activity_enrollment_form.html:168 +#: amelie/activities/templates/activity_enrollment_print.html:90 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:89 +#: amelie/activities/templates/includes/activity_participation_create_payment.html:45 +#: amelie/members/templates/person_membership_create_payment.html:45 #: amelie/members/templates/statistics/overview.html:44 #: amelie/members/templates/statistics/overview.html:133 -#: amelie/members/templates/statistics/payments.html:19 -#: amelie/personal_tab/templates/balance/balance.html:48 -#: amelie/personal_tab/templates/balance/balance.html:72 -#: amelie/personal_tab/templates/balance/balance.html:103 -#: amelie/personal_tab/templates/balance/balance.html:129 -#: amelie/personal_tab/templates/lists/activity_transactions.html:56 -#: amelie/personal_tab/templates/lists/alexia_transactions.html:42 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:48 -#: amelie/personal_tab/templates/lists/custom_transactions.html:42 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:50 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:48 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:21 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:50 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:38 -#: amelie/personal_tab/templates/lists/transactions.html:44 -#: amelie/personal_tab/templates/lists/transactions_totals.html:25 -#: amelie/personal_tab/templates/lists/transactions_totals.html:56 +#: amelie/personal_tab/templates/balance/balance.html:57 +#: amelie/personal_tab/templates/balance/balance.html:90 +#: amelie/personal_tab/templates/balance/balance.html:128 +#: amelie/personal_tab/templates/balance/balance.html:161 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:143 +#: amelie/personal_tab/templates/lists/activity_transactions.html:64 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:50 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:50 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:56 +#: amelie/personal_tab/templates/lists/custom_transactions.html:50 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:60 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:60 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:22 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:51 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:46 +#: amelie/personal_tab/templates/lists/payment_transactions.html:70 +#: amelie/personal_tab/templates/lists/transactions.html:24 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:39 +#: amelie/personal_tab/templates/lists/transactions_totals.html:27 +#: amelie/personal_tab/templates/lists/transactions_totals.html:66 #: amelie/personal_tab/templates/pos/shop.html:107 #: amelie/personal_tab/templates/statistics/statistics.html:120 #: amelie/personal_tab/templates/statistics/statistics.html:161 #: amelie/personal_tab/templates/statistics/statistics.html:216 #: amelie/personal_tab/templates/statistics/statistics.html:264 #: amelie/personal_tab/templates/statistics/statistics.html:299 -#: amelie/personal_tab/templates/statistics/statistics.html:355 -#: amelie/personal_tab/templates/statistics/statistics.html:389 -#: amelie/personal_tab/templates/statistics/statistics.html:435 +#: amelie/personal_tab/templates/statistics/statistics.html:354 +#: amelie/personal_tab/templates/statistics/statistics.html:387 +#: amelie/personal_tab/templates/statistics/statistics.html:433 #: amelie/personal_tab/templates/wrapped/wrapped.html:209 #: amelie/room_duty/templates/room_duty/table/overview.html:141 #: amelie/room_duty/templates/room_duty/table/overview.html:175 msgid "Total" msgstr "Totaal" -#: amelie/activities/templates/activity_enrollment_form.html:106 +#: amelie/activities/templates/activity_enrollment_form.html:175 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " If your print is for a committee, select it in the form above.\n" +#| " In that case the print will be free.\n" +#| " " +msgid "" +"\n" +" The costs will be deducted from %(name)s's personal tab\n" +" and will be paid via their direct debit mandate.\n" +" " +msgstr "" +"\n" +" Als je print voor een commissie, selecteer die dan in het bovenstaande formulier.\n" +"In dat geval zal de print gratis zijn.\n" +" " + +#: amelie/activities/templates/activity_enrollment_form.html:182 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Warning: This activity already has participants.\n" +#| " It is therefore impossible to add or alter options for enrollment.\n" +#| " Changing prices and/or texts will only influence people who have yet to enroll!\n" +#| " " +msgid "" +"\n" +" %(name)s has no direct debit mandate for activities.\n" +" The costs will be deducted from their personal tab balance and will\n" +" need to be paid manually, either at the activity or at the board.\n" +" " +msgstr "" +"\n" +"Let op: Activiteit heeft al deelnemers. Het is daarom niet mogelijk inschrijfopties toe te voegen of te verwijderen. Wijzigen van prijzen en/of hebben alleen invloed op de mensen die zich nog gaan inschrijven!" + +#: amelie/activities/templates/activity_enrollment_form.html:193 msgid "Update" msgstr "Aanpassen" -#: amelie/activities/templates/activity_enrollment_form.html:106 +#: amelie/activities/templates/activity_enrollment_form.html:193 #: amelie/activities/templates/activity_enrollment_person_search.html:30 -#: amelie/activities/templates/includes/activity_enrollment.html:169 +#: amelie/activities/templates/includes/activity_enrollment.html:166 msgid "Enroll" msgstr "Inschrijven" @@ -2072,11 +2472,19 @@ msgstr "" "Inschrijven voor %(name)s" #: amelie/activities/templates/activity_enrollment_mandate.html:20 +#, fuzzy +#| msgid "" +#| "\n" +#| "\t\t\t\t \tUnfortunately, you cannot enroll for this activity, because it has an entrance fee, and you have\n" +#| " not signed a direct debit mandate for consumptions and activities. Without this authorization, we\n" +#| " can not withdraw money from your account. Please ask the board for a mandate form.\n" +#| "\t\t\t\t" msgid "" "\n" -"\t\t\t\t \tUnfortunately, you cannot enroll for this activity, because it has an entrance fee, and you have\n" -" not signed a direct debit mandate for consumptions and activities. Without this authorization, we\n" -" can not withdraw money from your account. Please ask the board for a mandate form.\n" +"\t\t\t\t \tUnfortunately, you cannot enroll for this activity, because it has an entrance fee, or you have\n" +" selected a paid enrollment option. You do not have a signed direct debit mandate for\n" +" consumptions and activities. Without this authorization, we cannot withdraw money from\n" +" your account. Please ask the board for a mandate form.\n" "\t\t\t\t" msgstr "" "\n" @@ -2085,230 +2493,161 @@ msgstr "" "is het niet mogelijk om eventuele kosten af te schrijven. Vraag bij het bestuur om een\n" "machtigingsformulier." -#: amelie/activities/templates/activity_enrollment_mandate.html:28 +#: amelie/activities/templates/activity_enrollment_mandate.html:29 +#, fuzzy +#| msgid "" +#| "\n" +#| "\t\t Would you prefer not to sign a direct debit mandate? Please contact the board or organizing committee\n" +#| " and pay in cash.\n" +#| "\t\t " msgid "" "\n" -"\t\t Would you prefer not to sign a direct debit mandate? Please contact the board or organizing committee\n" -" and pay in cash.\n" +"\t\t Would you prefer not to sign a direct debit mandate? Please contact the board or organizing\n" +" committee to see if it's possible to pay manually by cash or PIN for this activity.\n" "\t\t " msgstr "" "\n" "\t\t Liever geen machtiging tekenen? Neem dan contact op met het bestuur of de organiserende commissie en betaal contant.\n" "\t\t " -#: amelie/activities/templates/activity_enrollment_overview.html:21 -msgid "For privacy reasons, this page only shows anonymized enrollment data. For more detailed information please export or print it." -msgstr "Op deze pagina vind je vanwege privacyoverwegingen alleen geanonimiseerde inschrijfgegevens. Voor meer gedetailleerde informatie moet je een export maken of de inschrijvingen printen." - -#: amelie/activities/templates/activity_enrollment_overview.html:27 -msgid "Number of total participants including waiting list" -msgstr "Aantal deelnemers inclusief wachtlijst: " - -#: amelie/activities/templates/activity_enrollment_overview.html:31 -msgid "Number of confirmed participants" -msgstr "Aantal bevestigde deelnemers: " - -#: amelie/activities/templates/activity_enrollment_overview.html:38 -msgid "Show all participants" -msgstr "Laat alle deelnemers zien" - -#: amelie/activities/templates/activity_enrollment_overview.html:40 -msgid "Only show underage participants" -msgstr "Laat alleen minderjarige deelnemers zien" - -#: amelie/activities/templates/activity_enrollment_overview.html:46 -msgid "Need a list of underage participants? Ask the board!" -msgstr "Heb je een lijst met minderjarige deelnemers nodig? Vraag het bestuur!" - -#: amelie/activities/templates/activity_enrollment_overview.html:55 -msgid "Your enrollments are visible to other members." -msgstr "Je inschrijvingen zijn zichtbaar voor andere leden." - -#: amelie/activities/templates/activity_enrollment_overview.html:57 -msgid "Your enrollments are anonymous." -msgstr "Je inschrijvingen zijn anoniem." - -#: amelie/activities/templates/activity_enrollment_overview.html:59 +#: amelie/activities/templates/activity_enrollment_person_search.html:10 +#, python-format msgid "" "\n" -" You can change this preference on your profile.\n" +" Enroll person for %(name)s\n" " " msgstr "" "\n" -" Je kunt deze voorkeur aanpassen op je profiel.\n" -" " +"Persoon inschrijven voor %(name)s" -#: amelie/activities/templates/activity_enrollment_overview.html:66 -msgid "You are currently only viewing underage participants!" -msgstr "Je bekijkt nu alleen minderjarige deelnemers!" +#: amelie/activities/templates/activity_enrollment_person_search.html:19 +#, python-format +msgid "" +"\n" +" Enroll person for %(name)s\n" +" " +msgstr "" +"\n" +"Persoon inschrijven voor %(name)s" -#: amelie/activities/templates/activity_enrollment_overview.html:76 -#: amelie/activities/templates/activity_enrollment_overview.html:228 -#: amelie/personal_tab/models.py:332 -msgid "Sequence" -msgstr "Volgorde" +#: amelie/activities/templates/activity_enrollment_print.html:19 +#, fuzzy, python-format +#| msgid "Enrollment for courses starts" +msgid "Enrollments for %(name)s (%(date)s)" +msgstr "Inschrijving vakken start" -#: amelie/activities/templates/activity_enrollment_overview.html:92 -#: amelie/members/models.py:1077 -#: amelie/members/templates/person_membership.html:47 +#: amelie/activities/templates/activity_enrollment_print.html:20 +#, fuzzy, python-format +#| msgid "Signature on %(date)s:" +msgid "(printed on %(date)s)" +msgstr "Was getekend op %(datum)s:" + +#: amelie/activities/templates/activity_enrollment_print.html:23 +msgid "This enrollment list contains personal data. Please do not leave this lying around somewhere and make sure that it is destroyed after use. If you find this list somewhere, please give it to the board of Inter-Actief." +msgstr "Deze inschrijflijst bevat persoonsgegevens. Laat deze a.u.b. niet slingeren en zorg dat deze naderhand wordt vernietigd. Mocht u deze lijst vinden, geef deze dan aan het bestuur van Inter-Actief." + +#: amelie/activities/templates/activity_enrollment_print.html:26 +msgid "Number of participants" +msgstr "Aantal deelnemers: " + +#: amelie/activities/templates/activity_enrollment_print.html:40 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:143 +#: amelie/activities/templates/includes/activity_participants_table.html:24 msgid "Payment" msgstr "Betaling" -#: amelie/activities/templates/activity_enrollment_overview.html:113 -#: amelie/activities/templates/activity_enrollment_overview.html:115 -#: amelie/activities/templates/activity_enrollment_overview.html:122 -msgid "This participant turns 18 during the activity!" -msgstr "Deze deelnemer wordt 18 tijdens de activiteit!" - -#: amelie/activities/templates/activity_enrollment_overview.html:120 -#: amelie/activities/templates/activity_enrollment_overview.html:124 -#: amelie/activities/templates/gallery_photo.html:43 -#: amelie/blame/templates/blame/blame_changelog.html:45 -#: amelie/blame/templates/blame/blame_detail.html:37 -#: amelie/blame/templates/blame/model_overview.html:42 -msgid "Anonymous" -msgstr "Anoniem" +#: amelie/activities/templates/activity_enrollment_print.html:55 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:172 +#: amelie/activities/templates/includes/activity_participants_table.html:65 +#: amelie/activities/templates/includes/activity_participants_table.html:137 +msgid "N/A (free)" +msgstr "" -#: amelie/activities/templates/activity_enrollment_overview.html:131 -msgid "By clicking here, you confirm to have received the full amount of money." -msgstr "Door hier te klikken, bevestig je het complete contante bedrag ontvangen te hebben." +#: amelie/activities/templates/activity_enrollment_print.html:59 +#: amelie/activities/templates/includes/activity_participants_table.html:79 +#: amelie/activities/templates/includes/activity_participants_table.html:141 +#: amelie/activities/templates/includes/activity_participation_payments.html:25 +#: amelie/members/templates/person_membership.html:84 +#: amelie/members/templates/person_membership_payments.html:25 +msgid "Paid" +msgstr "" -#: amelie/activities/templates/activity_enrollment_overview.html:131 -msgid "Register cash payment" -msgstr "Registreer contante betaling" +#: amelie/activities/templates/activity_enrollment_print.html:62 +#: amelie/activities/templates/activity_enrollment_print.html:64 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:175 +#: amelie/activities/templates/includes/activity_participants_table.html:83 +#: amelie/activities/templates/includes/activity_participants_table.html:85 +#: amelie/activities/templates/includes/activity_participants_table.html:144 +#: amelie/activities/templates/includes/activity_participants_table.html:146 +#: amelie/members/templates/person_membership.html:88 +#: amelie/members/templates/person_membership.html:90 +msgid "To be debited" +msgstr "" -#: amelie/activities/templates/activity_enrollment_overview.html:138 -#: amelie/activities/templates/activity_enrollment_overview.html:274 -#: amelie/activities/templates/includes/activity_enrollment.html:126 -msgid "Edit enrollment" -msgstr "Inschrijving aanpassen" +#: amelie/activities/templates/activity_enrollment_print.html:67 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:177 +#: amelie/members/templates/person_membership.html:127 +#, fuzzy +#| msgid "No payment" +msgid "Needs manual payment" +msgstr "Geen betaling" -#: amelie/activities/templates/activity_enrollment_overview.html:143 -#: amelie/activities/templates/activity_enrollment_overview.html:279 -#: amelie/activities/templates/activity_enrollment_print.html:53 -#: amelie/activities/templates/includes/activity_enrollment.html:124 -msgid "Deregister" -msgstr "Uitschrijven" +#: amelie/activities/templates/activity_enrollment_print.html:69 +#: amelie/activities/templates/includes/activity_participants_table.html:87 +#: amelie/activities/templates/includes/activity_participants_table.html:148 +#: amelie/activities/templates/includes/activity_participation_payments.html:27 +#: amelie/members/templates/person_membership.html:92 +#: amelie/members/templates/person_membership_payments.html:27 +#, fuzzy +#| msgid "Has not paid yet" +msgid "Not paid yet" +msgstr "Heeft nog niet betaald" -#: amelie/activities/templates/activity_enrollment_overview.html:164 +#: amelie/activities/templates/activity_enrollment_print.html:71 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:178 #, python-format -msgid "" -"\n" -" ...and %(count)s Anonymous member.\n" -" " -msgid_plural "" -"\n" -" ...and %(count)s Anonymous members.\n" -" " -msgstr[0] "" -"\n" -" ...en %(count)s Anoniem lid.\n" -" " -msgstr[1] "" -"\n" -" ...en %(count)s Anonieme leden.\n" -" " +msgid "Open amount: € %(costs)s" +msgstr "" -#: amelie/activities/templates/activity_enrollment_overview.html:180 -#: amelie/activities/templates/activity_enrollment_print.html:66 +#: amelie/activities/templates/activity_enrollment_print.html:83 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:80 msgid "Order totals" msgstr "Totalen bestellingen" -#: amelie/activities/templates/activity_enrollment_overview.html:183 -#: amelie/activities/templates/activity_enrollment_print.html:69 +#: amelie/activities/templates/activity_enrollment_print.html:86 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:83 msgid "Dish" msgstr "Gerecht" -#: amelie/activities/templates/activity_enrollment_overview.html:183 -#: amelie/activities/templates/activity_enrollment_print.html:69 +#: amelie/activities/templates/activity_enrollment_print.html:86 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:83 +#: amelie/activities/templates/includes/activity_participation_create_payment.html:18 +#: amelie/activities/templates/includes/activity_participation_payments.html:37 +#: amelie/members/templates/person_membership_create_payment.html:18 +#: amelie/members/templates/person_membership_payments.html:37 #: amelie/members/templates/statistics/overview.html:35 #: amelie/members/templates/statistics/overview.html:124 -#: amelie/personal_tab/forms.py:428 amelie/personal_tab/models.py:210 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:12 +#: amelie/personal_tab/forms.py:466 amelie/personal_tab/models.py:246 +#: amelie/personal_tab/templates/info/transaction_information.html:92 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:13 #: amelie/personal_tab/templates/pos/success.html:35 #: amelie/personal_tab/templates/statistics/statistics.html:57 #: amelie/personal_tab/templates/statistics/statistics.html:103 #: amelie/personal_tab/templates/statistics/statistics.html:144 #: amelie/personal_tab/templates/statistics/statistics.html:286 -#: amelie/personal_tab/templates/statistics/statistics.html:376 -#: amelie/personal_tab/templates/statistics/statistics.html:413 -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:31 -#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:18 +#: amelie/personal_tab/templates/statistics/statistics.html:374 +#: amelie/personal_tab/templates/statistics/statistics.html:411 +#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:27 #: amelie/personal_tab/templates/wrapped/wrapped.html:97 #: amelie/tools/pdf.py:570 msgid "Amount" msgstr "Aantal" -#: amelie/activities/templates/activity_enrollment_overview.html:183 -#: amelie/activities/templates/activity_enrollment_print.html:69 +#: amelie/activities/templates/activity_enrollment_print.html:86 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:83 msgid "Total price" msgstr "Totale prijs" -#: amelie/activities/templates/activity_enrollment_overview.html:192 -msgid "Grand Total" -msgstr "Eindtotaal" - -#: amelie/activities/templates/activity_enrollment_overview.html:203 -#: amelie/activities/templates/activity_enrollment_print.html:81 -msgid "Enroll people" -msgstr "Personen inschrijven" - -#: amelie/activities/templates/activity_enrollment_overview.html:208 -#: amelie/activities/templates/activity_enrollment_print.html:87 -#: amelie/members/templates/includes/query/query_mailing.html:4 -#: amelie/members/templates/includes/query/query_mailing.html:66 -#: amelie/members/templates/includes/query/query_mailing.html:144 -#: amelie/personal_tab/templates/debt_collection/view.html:46 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:72 -msgid "Send mailing" -msgstr "Mailing versturen" - -#: amelie/activities/templates/activity_enrollment_overview.html:213 -#: amelie/activities/templates/activity_enrollment_print.html:92 -msgid "View transactions" -msgstr "Transacties bekijken" - -#: amelie/activities/templates/activity_enrollment_overview.html:220 -#, python-format -msgid "Waiting list (%(n)s members)" -msgstr "Wachtlijst (%(n)s leden)" - -#: amelie/activities/templates/activity_enrollment_overview.html:220 -msgid "Locked" -msgstr "Vergrendeld" - -#: amelie/activities/templates/activity_enrollment_overview.html:235 -msgid "Enrollment #" -msgstr "Inschrijving #" - -#: amelie/activities/templates/activity_enrollment_person_search.html:10 -#, python-format -msgid "" -"\n" -" Enroll person for %(name)s\n" -" " -msgstr "" -"\n" -"Persoon inschrijven voor %(name)s" - -#: amelie/activities/templates/activity_enrollment_person_search.html:19 -#, python-format -msgid "" -"\n" -" Enroll person for %(name)s\n" -" " -msgstr "" -"\n" -"Persoon inschrijven voor %(name)s" - -#: amelie/activities/templates/activity_enrollment_print.html:18 -msgid "This enrollment list contains personal data. Please do not leave this lying around somewhere and make sure that it is destroyed after use. If you find this list somewhere, please give it to the board of Inter-Actief." -msgstr "Deze inschrijflijst bevat persoonsgegevens. Laat deze a.u.b. niet slingeren en zorg dat deze naderhand wordt vernietigd. Mocht u deze lijst vinden, geef deze dan aan het bestuur van Inter-Actief." - -#: amelie/activities/templates/activity_enrollment_print.html:21 -msgid "Number of participants" -msgstr "Aantal deelnemers: " - #: amelie/activities/templates/activity_list.html:9 msgid "Activities you don't want to miss" msgstr "Activiteiten die je niet mag missen" @@ -2339,7 +2678,7 @@ msgstr "iCal-document met activiteit" #: amelie/activities/templates/activity_list.html:70 #: amelie/activities/templates/activity_list.html:71 #: amelie/activities/templates/activity_list.html:127 -#: amelie/calendar/models.py:118 +#: amelie/calendar/models.py:194 #: amelie/companies/templates/companies/company_event_list.html:36 #: amelie/companies/templates/companies/company_event_list.html:86 #: templates/frontpage.html:28 templates/frontpage.html:29 @@ -2377,12 +2716,12 @@ msgstr "Oude activiteiten" #: amelie/claudia/templates/claudia/mapping_list.html:10 #: amelie/claudia/templates/claudia/mapping_list.html:36 #: amelie/companies/templates/companies/company_event_old.html:15 -#: amelie/education/templates/summaries.html:15 amelie/members/forms.py:380 +#: amelie/education/templates/summaries.html:15 amelie/members/forms.py:383 #: amelie/members/templates/query.html:34 #: amelie/members/templates/query.html:54 #: amelie/members/templates/query.html:67 #: amelie/members/templates/registration_check.html:21 -#: amelie/personal_tab/forms.py:148 +#: amelie/personal_tab/forms.py:180 #: amelie/personal_tab/templates/authorization/list.html:52 msgid "Search" msgstr "Zoek" @@ -2398,33 +2737,34 @@ msgstr "" "\n" "Resultaten (%(amount)s activiteiten)" -#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:21 +#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:25 msgid "Additional costs:" msgstr "Meerprijs:" -#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:25 +#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:29 +#: amelie/personal_tab/templates/statistics/statistics.html:145 msgid "Discount" msgstr "Korting:" -#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:28 -#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:28 +#: amelie/activities/templates/forms/enrollmentoption_checkbox_answer.html:32 +#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:31 msgid "spot(s) left" msgstr "plek(ken) over" -#: amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html:17 -#: amelie/activities/templates/forms/enrollmentoption_question_answer.html:16 +#: amelie/activities/templates/forms/enrollmentoption_foodquestion_answer.html:10 +#: amelie/activities/templates/forms/enrollmentoption_question_answer.html:9 msgid "required" msgstr "verplicht" -#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:21 +#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:24 msgid "Additional costs per unit:" msgstr "Meerprijs per stuk:" -#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:25 +#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:28 msgid "Discount per unit" msgstr "Korting per stuk:" -#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:32 +#: amelie/activities/templates/forms/enrollmentoption_numeric_answer.html:35 msgid "The maximum per person is" msgstr "Het maximum per persoon is " @@ -2501,6 +2841,15 @@ msgstr "Alle fotos" msgid "Photographer:" msgstr "Fotograaf: " +#: amelie/activities/templates/gallery_photo.html:43 +#: amelie/activities/templates/includes/activity_participants_table.html:52 +#: amelie/activities/templates/includes/activity_participants_table.html:56 +#: amelie/blame/templates/blame/blame_changelog.html:45 +#: amelie/blame/templates/blame/blame_detail.html:37 +#: amelie/blame/templates/blame/model_overview.html:42 +msgid "Anonymous" +msgstr "Anoniem" + #: amelie/activities/templates/gallery_photo.html:46 msgid "If you want to have this photo removed, please contact the board" msgstr "Als je deze foto wilt laten verwijderen, neem dan contact op met het bestuur" @@ -2619,164 +2968,528 @@ msgstr "" "\n" "Als je er ook bij wilt zijn moet je je inschrijven." -#: amelie/activities/templates/includes/activity_enrollment.html:64 +#: amelie/activities/templates/includes/activity_enrollment.html:64 +#, python-format +msgid "" +"\n" +" You can enroll for this activity until %(enddate)s at %(endtime)s.\n" +" " +msgstr "" +"\n" +" Inschrijven kan nog tot uiterlijk %(enddate)s om %(endtime)s.\n" +" " + +#: amelie/activities/templates/includes/activity_enrollment.html:70 +msgid "" +"\n" +" The maximum number of participants for this activity has been reached.\n" +" " +msgstr "" +"\n" +"Voor deze activiteit zijn geen plaatsen meer beschikbaar." + +#: amelie/activities/templates/includes/activity_enrollment.html:83 +msgid "You are on the waiting list for this event with the following options:" +msgstr "Je hebt je ingeschreven voor deze activiteit op de wachtlijst met de volgende inschrijfopties:" + +#: amelie/activities/templates/includes/activity_enrollment.html:85 +msgid "You have enrolled for this event with the following options:" +msgstr "Je hebt je ingeschreven voor deze activiteit met de volgende inschrijfopties:" + +#: amelie/activities/templates/includes/activity_enrollment.html:94 +#, python-format +msgid "" +"\n" +" %(title)s: You have not enrolled for this option\n" +" " +msgstr "" +"\n" +"%(title)s: Je bent niet ingeschreven voor deze optie" + +#: amelie/activities/templates/includes/activity_enrollment.html:109 +msgid "You have enrolled yourself for this event." +msgstr "Je hebt je ingeschreven voor deze activiteit." + +#: amelie/activities/templates/includes/activity_enrollment.html:111 +#: amelie/activities/templates/includes/activity_enrollment.html:190 +msgid "You are enrolled on the waiting list for this event." +msgstr "Je hebt je ingeschreven voor deze activiteit op de wachtlijst." + +#: amelie/activities/templates/includes/activity_enrollment.html:117 +#: amelie/activities/templates/includes/activity_enrollment.html:191 +msgid "Your spot on the waiting list is" +msgstr "Jouw plek op de wachtlijst is" + +#: amelie/activities/templates/includes/activity_enrollment.html:123 +msgid "Deregister from waiting list" +msgstr "Uitschrijven van de wachtlijst" + +#: amelie/activities/templates/includes/activity_enrollment.html:123 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:190 +#: amelie/activities/templates/includes/activity_participants_table.html:167 +msgid "Deregister" +msgstr "Uitschrijven" + +#: amelie/activities/templates/includes/activity_enrollment.html:126 +#: amelie/activities/templates/includes/activity_enrollment_overview.html:185 +#: amelie/activities/templates/includes/activity_participants_table.html:162 +msgid "Edit enrollment" +msgstr "Inschrijving aanpassen" + +#: amelie/activities/templates/includes/activity_enrollment.html:130 +#, python-format +msgid "" +"\n" +" You can reverse your enrollment till %(date)s at %(time)s.\n" +" " +msgstr "" +"\n" +"Je kunt je inschrijving nog terugtrekken tot uiterlijk %(date)s om %(time)s." + +#: amelie/activities/templates/includes/activity_enrollment.html:136 +msgid "For this activity, you can only unenroll yourself at the board." +msgstr "Voor deze activiteit kun je jezelf alleen uitschrijven indien je contact met het bestuur opneemt." + +#: amelie/activities/templates/includes/activity_enrollment.html:141 +msgid "" +"\n" +" You are not enrolled in this activity yet.\n" +" " +msgstr "" +"\n" +"Je doet nog niet mee met deze activiteit." + +#: amelie/activities/templates/includes/activity_enrollment.html:148 +msgid "" +"\n" +" It's not possible to unenroll yourself for this activity.\n" +" " +msgstr "" +"\n" +"Voor deze activiteit is het niet mogelijk jezelf uit te schrijven." + +#: amelie/activities/templates/includes/activity_enrollment.html:156 +msgid "" +"\n" +" You can no longer enroll, all placed have been filled.
    \n" +" Do you want to be on the waiting list? That is still possible!
    \n" +" When on the waiting list, you will be automatically enrolled as soon as someone unenrolls.\n" +" " +msgstr "" +"\n" +"Je kunt je niet meer inschrijven, alle plaatsen zijn vergeven.
    \n" +"Wil je op de reservelijst? Dat is nog wel mogelijk!
    \n" +"Wanneer je op de wachtlijst staat, word je vanzelf ingeschreven wanneer er plek is." + +#: amelie/activities/templates/includes/activity_enrollment.html:166 +msgid "Place on waiting list" +msgstr "Plaats op wachtlijst" + +#: amelie/activities/templates/includes/activity_enrollment.html:174 +#, python-format +msgid "" +"\n" +" To view or alter your enrollment, please log in.\n" +" " +msgstr "" +"\n" +"Om je inschrijving te bekijken of te wijzigen moet je inloggen." + +#: amelie/activities/templates/includes/activity_enrollment.html:183 +msgid "You can only enroll for this activity on the website of this activity" +msgstr "Je kan je voor deze activiteit alleen inschrijven via de website van deze activiteit" + +#: amelie/activities/templates/includes/activity_enrollment.html:187 +#: amelie/activities/templates/includes/activity_list_enrollment.html:5 +msgid "You have enrolled for this event" +msgstr "Je hebt je ingeschreven voor deze activiteit" + +#: amelie/activities/templates/includes/activity_enrollment.html:194 +msgid "Unfortunately, you cannot enroll for this activity anymore, as the enrollment period has ended." +msgstr "Je kunt je helaas niet meer inschrijven voor deze activiteit, de inschrijftermijn is verlopen." + +#: amelie/activities/templates/includes/activity_enrollment.html:198 +#, python-format +msgid "" +"\n" +" The enrollment for this activity has not been opened yet. You can enroll from %(enrollment_begin)s until %(enrollment_end)s.\n" +" " +msgstr "" +"\n" +"De inschrijving is helaas nog niet geopend voor deze activiteit. Van %(enrollment_begin)s tot %(enrollment_end)s kun je je inschrijven." + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:22 +msgid "For privacy reasons, this page only shows anonymized enrollment data. For more detailed information please export or print it." +msgstr "Op deze pagina vind je vanwege privacyoverwegingen alleen geanonimiseerde inschrijfgegevens. Voor meer gedetailleerde informatie moet je een export maken of de inschrijvingen printen." + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:28 +msgid "Number of total participants including waiting list" +msgstr "Aantal deelnemers inclusief wachtlijst: " + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:32 +msgid "Number of confirmed participants" +msgstr "Aantal bevestigde deelnemers: " + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:39 +msgid "Show all participants" +msgstr "Laat alle deelnemers zien" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:41 +msgid "Only show underage participants" +msgstr "Laat alleen minderjarige deelnemers zien" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:47 +msgid "Need a list of underage participants? Ask the board!" +msgstr "Heb je een lijst met minderjarige deelnemers nodig? Vraag het bestuur!" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:56 +msgid "Your enrollments are visible to other members." +msgstr "Je inschrijvingen zijn zichtbaar voor andere leden." + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:58 +msgid "Your enrollments are anonymous." +msgstr "Je inschrijvingen zijn anoniem." + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:60 +msgid "" +"\n" +" You can change this preference on your profile.\n" +" " +msgstr "" +"\n" +" Je kunt deze voorkeur aanpassen op je profiel.\n" +" " + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:67 +msgid "You are currently only viewing underage participants!" +msgstr "Je bekijkt nu alleen minderjarige deelnemers!" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:92 +msgid "Grand Total" +msgstr "Eindtotaal" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:103 +msgid "Enroll people" +msgstr "Personen inschrijven" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:108 +#: amelie/members/templates/includes/query/query_mailing.html:4 +#: amelie/members/templates/includes/query/query_mailing.html:66 +#: amelie/members/templates/includes/query/query_mailing.html:144 +#: amelie/personal_tab/templates/debt_collection/view.html:46 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:15 +msgid "Send mailing" +msgstr "Mailing versturen" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:113 +msgid "View transactions" +msgstr "Transacties bekijken" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:120 +#, python-format +msgid "Waiting list (%(n)s members)" +msgstr "Wachtlijst (%(n)s leden)" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:120 +msgid "Locked" +msgstr "Vergrendeld" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:128 +#: amelie/activities/templates/includes/activity_participants_table.html:8 +#: amelie/personal_tab/models.py:368 +msgid "Sequence" +msgstr "Volgorde" + +#: amelie/activities/templates/includes/activity_enrollment_overview.html:135 +msgid "Enrollment #" +msgstr "Inschrijving #" + +#: amelie/activities/templates/includes/activity_list_enrollment.html:7 +msgid "You are on the waiting list" +msgstr "Je staat op de wachtlijst" + +#: amelie/activities/templates/includes/activity_list_enrollment.html:10 +msgid "Subscription closed" +msgstr "Inschrijving gesloten" + +#: amelie/activities/templates/includes/activity_list_enrollment.html:12 +msgid "Enrollment is full" +msgstr "Inschrijving is vol" + +#: amelie/activities/templates/includes/activity_list_enrollment.html:14 +msgid "Enroll now!" +msgstr "Schrijf je nu in!" + +#: amelie/activities/templates/includes/activity_list_enrollment.html:16 #, python-format -msgid "" -"\n" -" You can enroll for this activity until %(enddate)s at %(endtime)s.\n" -" " +msgid "Subscription is open from %(date)s" +msgstr "Inschrijven kan vanaf %(date)s" + +#: amelie/activities/templates/includes/activity_participants_table.html:45 +#: amelie/activities/templates/includes/activity_participants_table.html:47 +#: amelie/activities/templates/includes/activity_participants_table.html:54 +msgid "This participant turns 18 during the activity!" +msgstr "Deze deelnemer wordt 18 tijdens de activiteit!" + +#: amelie/activities/templates/includes/activity_participants_table.html:69 +msgid "For some reason this free participation has unpaid payments! This should not happen." msgstr "" -"\n" -" Inschrijven kan nog tot uiterlijk %(enddate)s om %(endtime)s.\n" -" " -#: amelie/activities/templates/includes/activity_enrollment.html:70 +#: amelie/activities/templates/includes/activity_participants_table.html:71 +#: amelie/activities/templates/includes/activity_participants_table.html:91 +#: amelie/members/templates/person_membership.html:76 +#: amelie/members/templates/person_membership.html:96 +#, fuzzy +#| msgid "Execute payment" +msgid "Inspect payments" +msgstr "Betaling invoeren" + +#: amelie/activities/templates/includes/activity_participants_table.html:96 +#: amelie/members/templates/person_membership.html:101 +#, fuzzy +#| msgid "No transactions on personal tab." +msgid "No transaction! Contact the WWW." +msgstr "Geen streeplijsttransacties." + +#: amelie/activities/templates/includes/activity_participants_table.html:100 +#: amelie/members/templates/person_membership.html:105 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " (last used: %(last_used_date)s)\n" +#| " " msgid "" "\n" -" The maximum number of participants for this activity has been reached.\n" -" " +" Debited on %(date)s\n" +" " msgstr "" "\n" -"Voor deze activiteit zijn geen plaatsen meer beschikbaar." - -#: amelie/activities/templates/includes/activity_enrollment.html:83 -msgid "You are on the waiting list for this event with the following options:" -msgstr "Je hebt je ingeschreven voor deze activiteit op de wachtlijst met de volgende inschrijfopties:" - -#: amelie/activities/templates/includes/activity_enrollment.html:85 -msgid "You have enrolled for this event with the following options:" -msgstr "Je hebt je ingeschreven voor deze activiteit met de volgende inschrijfopties:" +" (laatst gebruikt: %(last_used_date)s)\n" +" " -#: amelie/activities/templates/includes/activity_enrollment.html:94 -#, python-format +#: amelie/activities/templates/includes/activity_participants_table.html:106 +#: amelie/members/templates/person_membership.html:111 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " (last used: %(last_used_date)s)\n" +#| " " msgid "" "\n" -" %(title)s: You have not enrolled for this option\n" -" " +" %(method)s on %(date)s\n" +" " msgstr "" "\n" -"%(title)s: Je bent niet ingeschreven voor deze optie" - -#: amelie/activities/templates/includes/activity_enrollment.html:109 -msgid "You have enrolled yourself for this event." -msgstr "Je hebt je ingeschreven voor deze activiteit." - -#: amelie/activities/templates/includes/activity_enrollment.html:111 -#: amelie/activities/templates/includes/activity_enrollment.html:193 -msgid "You are enrolled on the waiting list for this event." -msgstr "Je hebt je ingeschreven voor deze activiteit op de wachtlijst." +" (laatst gebruikt: %(last_used_date)s)\n" +" " -#: amelie/activities/templates/includes/activity_enrollment.html:117 -#: amelie/activities/templates/includes/activity_enrollment.html:194 -msgid "Your spot on the waiting list is" -msgstr "Jouw plek op de wachtlijst is" +#: amelie/activities/templates/includes/activity_participants_table.html:112 +#: amelie/members/templates/person_membership.html:117 +msgid "Unknown payment! Contact the WWW." +msgstr "" -#: amelie/activities/templates/includes/activity_enrollment.html:124 -msgid "Deregister from waiting list" -msgstr "Uitschrijven van de wachtlijst" +#: amelie/activities/templates/includes/activity_participants_table.html:117 +#: amelie/members/templates/person_membership.html:122 +msgid "Previous attempt was reversed" +msgstr "" -#: amelie/activities/templates/includes/activity_enrollment.html:131 +#: amelie/activities/templates/includes/activity_participants_table.html:121 +#: amelie/activities/templates/includes/activity_participants_table.html:151 #, python-format -msgid "" -"\n" -" You can reverse your enrollment till %(date)s at %(time)s.\n" -" " +msgid "Needs manual payment (€ %(costs)s)" msgstr "" -"\n" -"Je kunt je inschrijving nog terugtrekken tot uiterlijk %(date)s om %(time)s." -#: amelie/activities/templates/includes/activity_enrollment.html:137 -msgid "For this activity, you can only unenroll yourself at the board." -msgstr "Voor deze activiteit kun je jezelf alleen uitschrijven indien je contact met het bestuur opneemt." +#: amelie/activities/templates/includes/activity_participants_table.html:128 +#: amelie/members/templates/person_membership.html:135 +#, fuzzy +#| msgid "Confirmation payment" +msgid "Enter manual payment" +msgstr "Bevestigen betaling" -#: amelie/activities/templates/includes/activity_enrollment.html:142 +#: amelie/activities/templates/includes/activity_participants_table.html:188 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " ...and %(count)s Anonymous member.\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " ...and %(count)s Anonymous members.\n" +#| " " msgid "" "\n" -" You are not enrolled in this activity yet.\n" -" " -msgstr "" +" ...and %(count)s Anonymous member.\n" +" " +msgid_plural "" "\n" -"Je doet nog niet mee met deze activiteit." - -#: amelie/activities/templates/includes/activity_enrollment.html:150 -msgid "" +" ...and %(count)s Anonymous members.\n" +" " +msgstr[0] "" "\n" -" It's not possible to unenroll yourself for this activity.\n" -" " -msgstr "" +" ...en %(count)s Anoniem lid.\n" +" " +msgstr[1] "" "\n" -"Voor deze activiteit is het niet mogelijk jezelf uit te schrijven." +" ...en %(count)s Anonieme leden.\n" +" " -#: amelie/activities/templates/includes/activity_enrollment.html:158 +#: amelie/activities/templates/includes/activity_participation_create_payment.html:5 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Add credit for %(person)s.\n" +#| " " msgid "" "\n" -" You can no longer enroll, all placed have been filled.
    \n" -" Do you want to be on the waiting list? That is still possible!
    \n" -" When on the waiting list, you will be automatically enrolled as soon as someone unenrolls.\n" -" " +" Create manual payment for %(participation)s\n" +" " msgstr "" "\n" -"Je kunt je niet meer inschrijven, alle plaatsen zijn vergeven.
    \n" -"Wil je op de reservelijst? Dat is nog wel mogelijk!
    \n" -"Wanneer je op de wachtlijst staat, word je vanzelf ingeschreven wanneer er plek is." +"Tentamenkoekentegoed toevoegen voor %(person)s" -#: amelie/activities/templates/includes/activity_enrollment.html:169 -msgid "Place on waiting list" -msgstr "Plaats op wachtlijst" +#: amelie/activities/templates/includes/activity_participation_create_payment.html:10 +#: amelie/members/templates/person_membership_create_payment.html:10 +#, fuzzy +#| msgid "You are on the waiting list for this event with the following options:" +msgid "You are creating a manual payment that will cover the following transactions:" +msgstr "Je hebt je ingeschreven voor deze activiteit op de wachtlijst met de volgende inschrijfopties:" -#: amelie/activities/templates/includes/activity_enrollment.html:177 -#, python-format -msgid "" -"\n" -" To view or alter your enrollment, please log in.\n" -" " +#: amelie/activities/templates/includes/activity_participation_create_payment.html:16 +#: amelie/activities/templates/includes/activity_participation_payments.html:35 +#: amelie/blame/templates/blame/blame_changelog.html:25 +#: amelie/blame/templates/blame/blame_detail.html:29 +#: amelie/blame/templates/blame/model_overview.html:21 +#: amelie/members/templates/person_membership_create_payment.html:16 +#: amelie/members/templates/person_membership_payments.html:35 +#: amelie/personal_tab/templates/print/log_list.html:29 +#: amelie/personal_tab/templates/print/refund.html:14 amelie/tools/models.py:46 +#: amelie/tools/templates/data_export_statistics.html:26 +msgid "Timestamp" +msgstr "Tijdstempel" + +#: amelie/activities/templates/includes/activity_participation_create_payment.html:17 +#: amelie/activities/templates/includes/activity_participation_payments.html:36 +#: amelie/calendar/models.py:185 +#: amelie/claudia/templates/claudia/aliasgroup_list.html:23 +#: amelie/claudia/templates/claudia/contact_list.html:22 +#: amelie/claudia/templates/claudia/email_detail.html:18 +#: amelie/claudia/templates/claudia/extragroup_list.html:24 +#: amelie/claudia/templates/claudia/extraperson_list.html:23 +#: amelie/claudia/templates/claudia/kanidm/group_detail.html:18 +#: amelie/claudia/templates/claudia/kanidm/group_list.html:26 +#: amelie/claudia/templates/claudia/kanidm/service_account_detail.html:22 +#: amelie/claudia/templates/claudia/kanidm/service_account_list.html:25 +#: amelie/claudia/templates/claudia/mapping_detail.html:102 +#: amelie/claudia/templates/claudia/mapping_detail.html:157 +#: amelie/claudia/templates/claudia/shareddrive_list.html:22 +#: amelie/companies/templates/companies/company_event_form.html:87 +#: amelie/education/templates/education_event_form.html:50 +#: amelie/members/models.py:847 +#: amelie/members/templates/person_membership_create_payment.html:17 +#: amelie/members/templates/person_membership_payments.html:36 +#: amelie/oauth/templates/oauth_access_request.mail:12 +#: amelie/oauth/templates/request_oauth_sent.html:23 +#: amelie/personal_tab/forms.py:51 amelie/personal_tab/forms.py:474 +#: amelie/personal_tab/models.py:139 +#: amelie/personal_tab/templates/authorization/view.html:142 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:49 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:42 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:92 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:16 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:123 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:71 +#: amelie/personal_tab/templates/debt_collection/list.html:22 +#: amelie/personal_tab/templates/debt_collection/person_instructions.html:16 +#: amelie/personal_tab/templates/debt_collection/view.html:21 +#: amelie/personal_tab/templates/info/payments.html:22 +#: amelie/personal_tab/templates/info/payments.html:78 +#: amelie/personal_tab/templates/info/transaction_information.html:11 +#: amelie/personal_tab/templates/lists/activity_transactions.html:13 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:12 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:12 +#: amelie/personal_tab/templates/lists/custom_transactions.html:12 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:12 +#: amelie/personal_tab/templates/lists/payment_transactions.html:14 +#: amelie/personal_tab/templates/lists/transactions.html:12 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:12 +#: amelie/personal_tab/templates/statistics/statistics.html:322 +#: amelie/personal_tab/templates/wrapped/wrapped.html:239 +#: amelie/tools/pdf.py:584 amelie/videos/templates/videos/ia_video_form.html:58 +#: amelie/videos/templates/videos/peertube_video_form.html:58 +#: amelie/videos/templates/videos/yt_video_form.html:58 +msgid "Description" +msgstr "Omschrijving" + +#: amelie/activities/templates/includes/activity_participation_create_payment.html:28 +#: amelie/activities/templates/includes/activity_participation_payments.html:48 +#: amelie/members/templates/person_membership_create_payment.html:28 +#: amelie/members/templates/person_membership_payments.html:48 +#, fuzzy +#| msgid "Print membership forms (opens in new tab)" +msgid "Transaction details (opens in new tab)" +msgstr "Lidmaatschapsformulieren printen (opent in nieuwe tab)" + +#: amelie/activities/templates/includes/activity_participation_create_payment.html:39 +#: amelie/members/templates/person_membership_create_payment.html:39 +msgid "No transactions found. This is probably wrong! Please contact the WWW." msgstr "" -"\n" -"Om je inschrijving te bekijken of te wijzigen moet je inloggen." -#: amelie/activities/templates/includes/activity_enrollment.html:186 -msgid "You can only enroll for this activity on the website of this activity" -msgstr "Je kan je voor deze activiteit alleen inschrijven via de website van deze activiteit" +#: amelie/activities/templates/includes/activity_participation_create_payment.html:52 +#: amelie/members/templates/person_membership_create_payment.html:52 +msgid "Please select the payment method that was used for the payment." +msgstr "" -#: amelie/activities/templates/includes/activity_enrollment.html:190 -#: amelie/activities/templates/includes/activity_list_enrollment.html:5 -msgid "You have enrolled for this event" -msgstr "Je hebt je ingeschreven voor deze activiteit" +#: amelie/activities/templates/includes/activity_participation_create_payment.html:63 +#: amelie/members/templates/person_membership_create_payment.html:63 +#, fuzzy +#| msgid "Create page" +msgid "Create payment" +msgstr "Pagina aanmaken" -#: amelie/activities/templates/includes/activity_enrollment.html:197 -msgid "Unfortunately, you cannot enroll for this activity anymore, as the enrollment period has ended." -msgstr "Je kunt je helaas niet meer inschrijven voor deze activiteit, de inschrijftermijn is verlopen." +#: amelie/activities/templates/includes/activity_participation_payments.html:8 +#: amelie/members/templates/person_membership_payments.html:8 +msgid "Payment created. Please check the transactions below for correctness." +msgstr "" -#: amelie/activities/templates/includes/activity_enrollment.html:201 -#, python-format +#: amelie/activities/templates/includes/activity_participation_payments.html:14 +#: amelie/members/templates/person_membership_payments.html:14 +#, fuzzy +#| msgid "Invalid action. Please try again." +msgid "No unpaid transactions found. Please re-check and try again." +msgstr "Ongeldige actie. Probeer het opnieuw." + +#: amelie/activities/templates/includes/activity_participation_payments.html:20 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Add credit for %(person)s.\n" +#| " " msgid "" "\n" -" The enrollment for this activity has not been opened yet. You can enroll from %(enrollment_begin)s until %(enrollment_end)s.\n" -" " +" Payments for %(participation)s\n" +" " msgstr "" "\n" -"De inschrijving is helaas nog niet geopend voor deze activiteit. Van %(enrollment_begin)s tot %(enrollment_end)s kun je je inschrijven." - -#: amelie/activities/templates/includes/activity_list_enrollment.html:7 -msgid "You are on the waiting list" -msgstr "Je staat op de wachtlijst" - -#: amelie/activities/templates/includes/activity_list_enrollment.html:10 -msgid "Subscription closed" -msgstr "Inschrijving gesloten" +"Tentamenkoekentegoed toevoegen voor %(person)s" -#: amelie/activities/templates/includes/activity_list_enrollment.html:12 -msgid "Enrollment is full" -msgstr "Inschrijving is vol" +#: amelie/activities/templates/includes/activity_participation_payments.html:38 +#: amelie/members/templates/person_membership_payments.html:38 +#: amelie/personal_tab/templates/info/transaction_information.html:19 +#, fuzzy +#| msgid "Stay logged in" +msgid "Settled in" +msgstr "Blijf ingelogd" -#: amelie/activities/templates/includes/activity_list_enrollment.html:14 -msgid "Enroll now!" -msgstr "Schrijf je nu in!" +#: amelie/activities/templates/includes/activity_participation_payments.html:62 +msgid "No transactions found. If this is a paid activity, this is probably wrong! Please contact the WWW." +msgstr "" -#: amelie/activities/templates/includes/activity_list_enrollment.html:16 -#, python-format -msgid "Subscription is open from %(date)s" -msgstr "Inschrijven kan vanaf %(date)s" +#: amelie/activities/templates/includes/activity_participation_payments.html:70 +#: amelie/members/templates/person_mandate_retrieve_signed.html:18 +#: amelie/members/templates/person_membership_payments.html:70 +#: amelie/members/templates/person_membership_retrieve_signed.html:18 +msgid "Return" +msgstr "Terug" #: amelie/activities/templates/includes/gallery_paginator.html:5 #: amelie/activities/templates/includes/gallery_paginator.html:8 @@ -2920,232 +3633,236 @@ msgstr "Er zijn geen bestanden in de uploadmap" msgid "Upload pictures" msgstr "Foto's uploaden" -#: amelie/activities/utils.py:34 -#, python-brace-format -msgid "Enrollment for {activity} is not possible. You do not have an active membership at Inter-Actief" -msgstr "Inschrijven voor {activity} is niet mogelijk. Je hebt geen actief lidmaatschap bij Inter-Actief." - -#: amelie/activities/utils.py:43 amelie/activities/utils.py:101 -#: amelie/activities/utils.py:133 -#, python-brace-format -msgid "Unenrollment for {activity} is not possible." -msgstr "Uitschrijven voor {activity} is niet mogelijk." - -#: amelie/activities/utils.py:50 -#, python-brace-format -msgid "Enrollment for {activity} is only possible on the website of this activity." -msgstr "Inschrijven voor {activity} is alleen mogelijk op de website van deze activiteit." - -#: amelie/activities/utils.py:58 -#, python-brace-format -msgid "The enrollment period for {activity} hasn't started yet or has already passed." -msgstr "De inschrijfperiode voor {activity} is nog niet begonnen of is al voorbij." - -#: amelie/activities/utils.py:66 -#, python-brace-format -msgid "{person} is already enrolled for {activity}." -msgstr "{person} is al ingeschreven voor {activity}." - -#: amelie/activities/utils.py:72 -#, python-brace-format -msgid "You are already enrolled for {activity}." -msgstr "Je bent al ingeschreven voor {activity}." - -#: amelie/activities/utils.py:108 -#, python-brace-format -msgid "Unenrollment for {activity} is only possible on the website of this activity." -msgstr "Uitschrijven voor {activity} is alleen mogelijk op de website van deze activiteit." - -#: amelie/activities/utils.py:120 -#, python-brace-format -msgid "{person} is not enrolled for {activity}." -msgstr "{person} is niet ingeschreven voor {activity}." - -#: amelie/activities/utils.py:126 -#, python-brace-format -msgid "You are not enrolled for {activity}." -msgstr "Je bent niet ingeschreven voor {activity}." - -#: amelie/activities/utils.py:140 -#, python-brace-format -msgid "The unenrollment term for {activity} has already passed." -msgstr "De uitschrijfperiode voor {activity} is al voorbij." - -#: amelie/activities/utils.py:164 -#, python-brace-format -msgid "{person} has been promoted from the waiting list for {activity}." -msgstr "{person} is gepromoveerd van de wachtlijst naar bevestigde deelname voor {activity}." - -#: amelie/activities/views.py:102 +#: amelie/activities/views.py:76 msgid "Events of Inter-Actief" msgstr "Activiteiten IA" -#: amelie/activities/views.py:285 amelie/activities/views.py:1083 -msgid "No photo" -msgstr "Geen foto" - -#: amelie/activities/views.py:339 +#: amelie/activities/views.py:331 #, python-brace-format msgid "{obj} has been successfully re-opened. Please note that any previous enrollments are not added again and that enrollments will have to be manually re-opened." msgstr "{obj} is succesvol heropend. Let erop dat vorige aanmeldingen niet opnieuw zijn toegevoegd en dat je inschrijvingen weer handmatig open moet zetten." -#: amelie/activities/views.py:346 +#: amelie/activities/views.py:339 +#, python-brace-format +msgid "{obj} was cancelled successfully." +msgstr "{obj} is succesvol geannuleerd." + +#: amelie/activities/views.py:344 #, python-brace-format msgid "We were unable to cancel {obj}, since it has already started." msgstr "Het annuleren van {obj} is niet meer mogelijk, omdat deze al is begonnen." -#: amelie/activities/views.py:370 -#, python-brace-format -msgid "{obj} was cancelled successfully." -msgstr "{obj} is succesvol geannuleerd." +#: amelie/activities/views.py:387 +#, fuzzy +#| msgid "You are not allowed to edit this activity" +msgid "You are not allowed to access this activity page for other people." +msgstr "Je mag deze activiteit niet bewerken" + +#: amelie/activities/views.py:390 +msgid "Not implemented on base enrollment view." +msgstr "" + +#: amelie/activities/views.py:452 +#, fuzzy +#| msgid "You are not allowed to edit this activity" +msgid "You are not allowed to enroll other people for this activity." +msgstr "Je mag deze activiteit niet bewerken" + +#: amelie/activities/views.py:493 amelie/activities/views.py:690 +#, fuzzy, python-brace-format +#| msgid "{person} is now enrolled for {waiting_list} {activity}." +msgid "{person} is now enrolled for the waitinglist of {activity}." +msgstr "{person} is nu ingeschreven voor {waiting_list} {activity}." + +#: amelie/activities/views.py:495 amelie/activities/views.py:692 +#, fuzzy, python-brace-format +#| msgid "{person} is now unenrolled for {activity}." +msgid "{person} is now enrolled for {activity}." +msgstr "{person} is nu uitgeschreven voor {activity}." + +#: amelie/activities/views.py:499 amelie/activities/views.py:696 +#, fuzzy, python-brace-format +#| msgid "You are now enrolled for {waiting_list} {activity}." +msgid "You are now enrolled for the waitinglist of {activity}." +msgstr "Je bent nu ingeschreven voor {waiting_list} {activity}." -#: amelie/activities/views.py:408 +#: amelie/activities/views.py:501 amelie/activities/views.py:698 #, python-brace-format msgid "You are now enrolled for {activity}." msgstr "Je bent nu ingeschreven voor {activity}." -#: amelie/activities/views.py:414 +#: amelie/activities/views.py:562 amelie/activities/views.py:731 +msgid "Your changes would make the costs of your participation negative! Unfortunately that's not allowed, so the change was blocked. Please contact the board or the organizing committee if you think this is a mistake." +msgstr "" + +#: amelie/activities/views.py:598 +#, fuzzy +#| msgid "You are not allowed to edit this activity" +msgid "You are not allowed to unenroll other people for this activity." +msgstr "Je mag deze activiteit niet bewerken" + +#: amelie/activities/views.py:620 #, python-brace-format -msgid "You are now on the waiting list for {activity}." -msgstr "Je staat nu op de wachtlijst voor {activity}." +msgid "{person} is now unenrolled for {activity}." +msgstr "{person} is nu uitgeschreven voor {activity}." -#: amelie/activities/views.py:450 +#: amelie/activities/views.py:624 #, python-brace-format msgid "You are now unenrolled for {activity}." msgstr "Je bent nu uitgeschreven voor {activity}." -#: amelie/activities/views.py:579 +#: amelie/activities/views.py:631 #, python-brace-format -msgid "{person} is now unenrolled for {activity}." -msgstr "{person} is nu uitgeschreven voor {activity}." +msgid "{person} has been promoted from the waiting list for {activity}." +msgstr "{person} is gepromoveerd van de wachtlijst naar bevestigde deelname voor {activity}." -#: amelie/activities/views.py:770 -msgid "the waitinglist of" -msgstr "de wachtlijst voor" +#: amelie/activities/views.py:651 +#, fuzzy +#| msgid "You are not allowed to edit this activity" +msgid "You are not allowed to edit enrollments of other people for this activity." +msgstr "Je mag deze activiteit niet bewerken" -#: amelie/activities/views.py:774 -#, python-brace-format -msgid "{person} is now enrolled for {waiting_list} {activity}." -msgstr "{person} is nu ingeschreven voor {waiting_list} {activity}." +#: amelie/activities/views.py:710 +msgid "Participation not found while editing enrollment." +msgstr "" -#: amelie/activities/views.py:781 -#, python-brace-format -msgid "You are now enrolled for {waiting_list} {activity}." -msgstr "Je bent nu ingeschreven voor {waiting_list} {activity}." +#: amelie/activities/views.py:757 amelie/activities/views.py:1045 +msgid "No photo" +msgstr "Geen foto" -#: amelie/activities/views.py:852 +#: amelie/activities/views.py:814 msgid "The photos are now being processed in the background.Photos will show up on the activity as they are processed, you can refresh the page to follow the progres." msgstr "De foto's worden nu in de achtergrond verwerkt. Foto's zullen per foto bij de activiteit verschijnen terwijl ze verwerkt worden, je kan de pagina verversen om de voortgang te volgen." -#: amelie/activities/views.py:871 amelie/activities/views.py:876 -#: amelie/activities/views.py:880 +#: amelie/activities/views.py:833 amelie/activities/views.py:838 +#: amelie/activities/views.py:842 msgid "This picture does not exist." msgstr "Deze foto bestaat niet." -#: amelie/activities/views.py:1098 +#: amelie/activities/views.py:1060 msgid "No participants" msgstr "Geen deelnemers" -#: amelie/activities/views.py:1151 +#: amelie/activities/views.py:1113 msgid "You do not have permissions to edit this activity" msgstr "Je mag deze activiteit niet bewerken" -#: amelie/activities/views.py:1154 +#: amelie/activities/views.py:1116 msgid "You cannot edit this activity, because it is cancelled" msgstr "Je kunt deze activiteit niet aanpassen, omdat hij is geannuleerd" -#: amelie/activities/views.py:1175 +#: amelie/activities/views.py:1137 msgid "You did not fill in an English summary/description." msgstr "Je hebt geen Engelse samenvatting/omschrijving ingevuld." -#: amelie/activities/views.py:1176 amelie/activities/views.py:1183 +#: amelie/activities/views.py:1138 amelie/activities/views.py:1145 msgid "edit event" msgstr "activiteit wijzigen" -#: amelie/activities/views.py:1182 +#: amelie/activities/views.py:1144 msgid "You did not fill in a Dutch summary/description." msgstr "Je hebt geen Nederlandse samenvatting/omschrijving ingevuld." -#: amelie/activities/views.py:1232 +#: amelie/activities/views.py:1194 +msgid "Editing the activity price to this value would make the minimum possible price for this activity negative! This is not allowed. Please modify the pricing and limits of the activity or the enrollment options first." +msgstr "" + +#: amelie/activities/views.py:1207 msgid "start date was changed" msgstr "startdatum is aangepast" -#: amelie/activities/views.py:1237 +#: amelie/activities/views.py:1213 msgid "price was changed" msgstr "prijs is aangepast" -#: amelie/activities/views.py:1247 +#: amelie/activities/views.py:1219 #, python-brace-format msgid "Activity '{activity}' was changed (reversal of old transaction) ({change_reasons})" msgstr "Activiteit '{activity}' is aangepast (terugboeken van oude transactie) ({change_reasons})" -#: amelie/activities/views.py:1259 +#: amelie/activities/views.py:1238 +msgid "Editing the activity in this way would make the costs of (at least) one of the current enrollments negative! This is not allowed. Please unenroll the affected people, or modify the pricing and limits of the activity or the enrollment options first." +msgstr "" + +#: amelie/activities/views.py:1243 #, python-brace-format msgid "Activity '{activity}' was changed (addition of new transaction) ({change_reasons})" msgstr "Activiteit '{activity}' is aangepast (toevoegen van nieuwe transactie) ({change_reasons})" -#: amelie/activities/views.py:1328 amelie/activities/views.py:1338 +#: amelie/activities/views.py:1318 +msgid "Adding this option would make the minimum possible price for this activity negative! This is not allowed. Please modify the pricing or limits of this or other enrollment options first." +msgstr "" + +#: amelie/activities/views.py:1332 amelie/activities/views.py:1342 msgid "This activity already has enrollments, therefore options can no longer be added!" msgstr "Deze activiteit heeft inschrijvingen, er kunnen geen opties toegevoegd worden!" -#: amelie/activities/views.py:1370 +#: amelie/activities/views.py:1374 +msgid "Editing this option in this way would make the minimum possible price for this activity negative! This is not allowed. Please modify the pricing or limits of this or other enrollment options first." +msgstr "" + +#: amelie/activities/views.py:1384 msgid "enrollment option price was changed" msgstr "prijs van inschrijvingsoptie gewijzigd" -#: amelie/activities/views.py:1380 +#: amelie/activities/views.py:1390 #, python-brace-format msgid "An enrollment option for activity '{activity}' was changed (reversal of old transaction) ({change_reasons})" msgstr "Een inschrijfoptie voor activiteit '{activity}' is aangepast (terugboeken van de oude transactie) ({change_reasons})" -#: amelie/activities/views.py:1392 +#: amelie/activities/views.py:1409 +msgid "Editing this option in this way would make the costs of (at least) one of the current enrollments negative! This is not allowed. Please unenroll the affected people, or modify the pricing or limits of this or other enrollment options first." +msgstr "" + +#: amelie/activities/views.py:1414 #, python-brace-format msgid "An enrollment option for activity '{activity}' was changed (addition of new transaction) ({change_reasons})" msgstr "Een inschrijfoptie voor activiteit '{activity}' is aangepast (toevoegen van nieuwe transactie) ({change_reasons})" -#: amelie/activities/views.py:1432 amelie/activities/views.py:1446 +#: amelie/activities/views.py:1460 amelie/activities/views.py:1474 msgid "This activity already has enrollments, therefore options cannot be deleted!" msgstr "Deze activiteit heeft inschrijvingen, er kunnen geen opties verwijderd worden!" -#: amelie/activities/views.py:1438 +#: amelie/activities/views.py:1466 #, python-format msgid "Enrollment options %(object)s has been deleted" msgstr "Inschrijfoptie %(object)s is verwijderd" -#: amelie/activities/views.py:1459 +#: amelie/activities/views.py:1487 msgid "Only accessible for the board and the organisers of this activity." msgstr "Uitsluitend toegankelijk voor het bestuur en de organisators van de activiteit." -#: amelie/activities/views.py:1465 amelie/activities/views.py:1480 +#: amelie/activities/views.py:1493 amelie/activities/views.py:1508 msgid "No activity found or invalid activity ID given." msgstr "Geen activiteit gevonden of je hebt een ongeldige activiteit-id meegegeven." -#: amelie/activities/views.py:1475 +#: amelie/activities/views.py:1503 msgid "Invalid activity ID" msgstr "Ongeldige activiteit-id." -#: amelie/activities/views.py:1496 amelie/personal_tab/views.py:1626 +#: amelie/activities/views.py:1524 amelie/personal_tab/views.py:2037 msgid "Could not create data export, because an unknown data-exporttype is provided." msgstr "Kon geen data-export maken, omdat er een onbekend data-exporttype is opegeven." -#: amelie/activities/views.py:1503 amelie/members/query_views.py:241 -#: amelie/personal_tab/views.py:1633 +#: amelie/activities/views.py:1531 amelie/members/query_views.py:241 +#: amelie/personal_tab/views.py:2044 msgid "Could not create data export, because something went wrong while saving information about the export." msgstr "Kon geen data-export maken, omdat er iets mis ging bij het opslaan van gegevens over de data-export." -#: amelie/activities/views.py:1581 amelie/activities/views.py:1591 -#: amelie/activities/views.py:1593 +#: amelie/activities/views.py:1612 amelie/activities/views.py:1622 +#: amelie/activities/views.py:1624 msgid "You are not allowed to edit this activity" msgstr "Je mag deze activiteit niet bewerken" -#: amelie/activities/views.py:1633 +#: amelie/activities/views.py:1664 msgid "Unknown error occurred." msgstr "Er is een onbekende fout opgetreden." -#: amelie/activities/views.py:1649 amelie/activities/views.py:1664 +#: amelie/activities/views.py:1680 amelie/activities/views.py:1695 msgid "Message is not encoded in UTF-8, Latin-1 or ASCII, giving up..." msgstr "Bericht is niet geëncodeerd in UTF-8, Latin-1 of ASCII, ik geef het op..." -#: amelie/activities/views.py:1670 +#: amelie/activities/views.py:1701 msgid "Could not retrieve this message from Google. {}" msgstr "Kon dit bericht niet ophalen bij Google. {}" @@ -3194,15 +3911,6 @@ msgstr "Object" msgid "Action" msgstr "Actie" -#: amelie/blame/templates/blame/blame_changelog.html:25 -#: amelie/blame/templates/blame/blame_detail.html:29 -#: amelie/blame/templates/blame/model_overview.html:21 -#: amelie/personal_tab/templates/print/log_list.html:29 -#: amelie/personal_tab/templates/print/refund.html:14 amelie/tools/models.py:46 -#: amelie/tools/templates/data_export_statistics.html:26 -msgid "Timestamp" -msgstr "Tijdstempel" - #: amelie/blame/templates/blame/blame_changelog.html:26 #: amelie/blame/templates/blame/blame_detail.html:62 #: amelie/blame/templates/blame/model_overview.html:22 @@ -3294,6 +4002,7 @@ msgid "Detail page for this change" msgstr "Detailpagina van deze wijziging" #: amelie/blame/templates/blame/overview.html:4 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:21 #: amelie/room_duty/templates/room_duty/table/overview.html:6 #: amelie/room_duty/templates/room_duty/table/overview.html:86 msgid "Overview" @@ -3323,48 +4032,42 @@ msgstr "Promotietekst voor deze activiteit, in de voorkeurstaal van de gebruiker msgid "A brief description of this activity (always in english)." msgstr "Een korte beschrijving van deze activiteit (altijd in het Engels)." -#: amelie/calendar/models.py:26 -msgid "No payment" -msgstr "Geen betaling" - -#: amelie/calendar/models.py:27 -msgid "Cash at the board/committee" -msgstr "Contant bij bestuur/commissie" +#: amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py:170 +#, python-brace-format +msgid "Manual payment required for enrollment to {event} on {date} (migrated)" +msgstr "" -#: amelie/calendar/models.py:28 amelie/members/query_forms.py:83 -#: amelie/members/templates/includes/query/query_mandate.html:4 -#: amelie/personal_tab/templates/authorization/view.html:4 -#: amelie/personal_tab/templates/authorization/view.html:8 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:36 -#: amelie/personal_tab/templates/debt_collection/process_batch.html:49 -#: amelie/personal_tab/templates/debt_collection/view.html:92 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:11 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:10 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:24 -msgid "Mandate" -msgstr "Machtiging" +#: amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py:190 +#, python-brace-format +msgid "Manual payment for enrollment to {event} on {date} (migrated)" +msgstr "" #: amelie/calendar/models.py:30 amelie/data_export/models.py:31 -#: amelie/education/models.py:289 amelie/members/forms.py:375 -#: amelie/members/models.py:867 amelie/members/models.py:918 -#: amelie/members/models.py:1286 +#: amelie/education/models.py:289 amelie/members/forms.py:378 +#: amelie/members/models.py:878 amelie/members/models.py:929 +#: amelie/members/models.py:1364 #: amelie/members/templates/committee_edit_members.html:33 #: amelie/members/templates/committee_edit_single_member.html:28 -#: amelie/personal_tab/forms.py:139 amelie/personal_tab/forms.py:193 -#: amelie/personal_tab/models.py:133 amelie/personal_tab/models.py:380 +#: amelie/personal_tab/forms.py:171 amelie/personal_tab/forms.py:225 +#: amelie/personal_tab/models.py:138 amelie/personal_tab/models.py:416 #: amelie/personal_tab/templates/authorization/view.html:21 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:19 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:41 #: amelie/personal_tab/templates/debt_collection/instruction_view.html:23 #: amelie/personal_tab/templates/debt_collection/process_batch.html:48 #: amelie/personal_tab/templates/debt_collection/view.html:91 -#: amelie/personal_tab/templates/info/transaction_information.html:39 -#: amelie/personal_tab/templates/lists/activity_transactions.html:10 -#: amelie/personal_tab/templates/lists/alexia_transactions.html:10 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:10 -#: amelie/personal_tab/templates/lists/custom_transactions.html:10 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:9 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:9 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:10 -#: amelie/personal_tab/templates/lists/transactions.html:10 +#: amelie/personal_tab/templates/info/transaction_information.html:36 +#: amelie/personal_tab/templates/lists/activity_transactions.html:11 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:11 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:11 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:11 +#: amelie/personal_tab/templates/lists/custom_transactions.html:11 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:10 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:10 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:11 +#: amelie/personal_tab/templates/lists/payment_transactions.html:12 +#: amelie/personal_tab/templates/lists/transactions.html:11 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:11 #: amelie/personal_tab/templates/statistics/statistics.html:321 #: templates/profile_picture_verification.html:97 msgid "Person" @@ -3372,144 +4075,105 @@ msgstr "Persoon" #: amelie/calendar/models.py:32 amelie/education/models.py:254 #: amelie/education/models.py:290 amelie/education/templates/complaint.html:107 -#: amelie/members/models.py:1289 amelie/personal_tab/forms.py:118 -#: amelie/personal_tab/templates/authorization/view.html:70 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:138 -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:29 +#: amelie/members/models.py:1367 amelie/personal_tab/forms.py:116 +#: amelie/personal_tab/templates/authorization/view.html:75 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:143 +#: amelie/personal_tab/templates/info/transaction_information.html:77 msgid "Remarks" msgstr "Opmerking" -#: amelie/calendar/models.py:35 -msgid "Cash payment made" -msgstr "Contante betaling voldaan" - -#: amelie/calendar/models.py:36 +#: amelie/calendar/models.py:33 msgid "On waiting list" msgstr "Op wachtlijst" -#: amelie/calendar/models.py:38 amelie/personal_tab/models.py:140 -#: amelie/personal_tab/templates/info/transaction_information.html:49 +#: amelie/calendar/models.py:35 amelie/personal_tab/models.py:145 +#: amelie/personal_tab/templates/info/transaction_information.html:53 msgid "Added on" msgstr "Toegevoegd op" -#: amelie/calendar/models.py:41 amelie/personal_tab/models.py:143 -#: amelie/personal_tab/templates/info/transaction_information.html:53 +#: amelie/calendar/models.py:38 amelie/personal_tab/models.py:148 +#: amelie/personal_tab/templates/info/transaction_information.html:57 msgid "Added by" msgstr "Toegevoegd door" -#: amelie/calendar/models.py:45 +#: amelie/calendar/models.py:42 msgid "Participation" msgstr "Deelname" -#: amelie/calendar/models.py:46 +#: amelie/calendar/models.py:43 msgid "Participations" msgstr "Deelnames" -#: amelie/calendar/models.py:52 amelie/data_export/exporters/amelie.py:137 +#: amelie/calendar/models.py:49 amelie/data_export/exporters/amelie.py:158 msgid "deleted activity" msgstr "verwijderde activiteit" -#: amelie/calendar/models.py:94 amelie/education/models.py:134 +#: amelie/calendar/models.py:136 +#, python-brace-format +msgid "Payment for enrollment to {event} on {date}" +msgstr "" + +#: amelie/calendar/models.py:170 amelie/education/models.py:134 #: amelie/room_duty/models.py:40 amelie/room_duty/models.py:109 #: amelie/room_duty/models.py:206 msgid "Starts" msgstr "Begint" -#: amelie/calendar/models.py:95 amelie/education/models.py:135 +#: amelie/calendar/models.py:171 amelie/education/models.py:135 #: amelie/room_duty/models.py:110 amelie/room_duty/models.py:207 msgid "Ends" msgstr "Eindigt" -#: amelie/calendar/models.py:96 +#: amelie/calendar/models.py:172 msgid "All day" msgstr "Hele dag" -#: amelie/calendar/models.py:98 amelie/education/models.py:253 +#: amelie/calendar/models.py:174 amelie/education/models.py:253 msgid "Summary" msgstr "Samenvatting" -#: amelie/calendar/models.py:99 +#: amelie/calendar/models.py:175 msgid "Summary (en)" msgstr "Samenvatting (en)" -#: amelie/calendar/models.py:101 amelie/calendar/models.py:104 +#: amelie/calendar/models.py:177 amelie/calendar/models.py:180 msgid "Short promotional message" msgstr "Korte promotietekst" -#: amelie/calendar/models.py:102 amelie/calendar/models.py:106 +#: amelie/calendar/models.py:178 amelie/calendar/models.py:182 msgid "This text can be used by the board for promotion, for example on our socials or in our weekmail. Let it be a teaser, so people would want to read your full activity description." msgstr "Deze tekst kan gebruikt worden door het bestuur voor promotie, bijvoorbeeld op onze social media kanalen of in onze weekmail. Laat het een teaser zijn, zodat mensen die geïnteresseerd zijn jouw volledige beschrijving willen lezen." -#: amelie/calendar/models.py:109 -#: amelie/claudia/templates/claudia/aliasgroup_list.html:23 -#: amelie/claudia/templates/claudia/contact_list.html:22 -#: amelie/claudia/templates/claudia/email_detail.html:18 -#: amelie/claudia/templates/claudia/extragroup_list.html:24 -#: amelie/claudia/templates/claudia/extraperson_list.html:23 -#: amelie/claudia/templates/claudia/kanidm/group_detail.html:18 -#: amelie/claudia/templates/claudia/kanidm/group_list.html:26 -#: amelie/claudia/templates/claudia/kanidm/service_account_detail.html:22 -#: amelie/claudia/templates/claudia/kanidm/service_account_list.html:25 -#: amelie/claudia/templates/claudia/mapping_detail.html:102 -#: amelie/claudia/templates/claudia/mapping_detail.html:157 -#: amelie/claudia/templates/claudia/shareddrive_list.html:22 -#: amelie/companies/templates/companies/company_event_form.html:87 -#: amelie/education/templates/education_event_form.html:50 -#: amelie/members/models.py:820 amelie/members/models.py:849 -#: amelie/oauth/templates/oauth_access_request.mail:12 -#: amelie/oauth/templates/request_oauth_sent.html:23 -#: amelie/personal_tab/forms.py:52 amelie/personal_tab/forms.py:436 -#: amelie/personal_tab/models.py:134 -#: amelie/personal_tab/templates/authorization/view.html:137 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:66 -#: amelie/personal_tab/templates/debt_collection/list.html:22 -#: amelie/personal_tab/templates/debt_collection/person_instructions.html:16 -#: amelie/personal_tab/templates/debt_collection/view.html:21 -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:18 -#: amelie/personal_tab/templates/info/transaction_information.html:7 -#: amelie/personal_tab/templates/lists/activity_transactions.html:12 -#: amelie/personal_tab/templates/lists/alexia_transactions.html:11 -#: amelie/personal_tab/templates/lists/custom_transactions.html:11 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:11 -#: amelie/personal_tab/templates/lists/transactions.html:11 -#: amelie/personal_tab/templates/statistics/statistics.html:322 -#: amelie/personal_tab/templates/wrapped/wrapped.html:239 -#: amelie/tools/pdf.py:584 amelie/videos/templates/videos/ia_video_form.html:58 -#: amelie/videos/templates/videos/peertube_video_form.html:58 -#: amelie/videos/templates/videos/yt_video_form.html:58 -msgid "Description" -msgstr "Omschrijving" - -#: amelie/calendar/models.py:110 +#: amelie/calendar/models.py:186 msgid "Description (en)" msgstr "Omschrijving (en)" -#: amelie/calendar/models.py:112 +#: amelie/calendar/models.py:188 msgid "Organizer" msgstr "Organisator" -#: amelie/calendar/models.py:115 +#: amelie/calendar/models.py:191 msgid "Participants" msgstr "Deelnemers" -#: amelie/calendar/models.py:117 amelie/files/models.py:149 -#: amelie/personal_tab/forms.py:441 amelie/tools/pdf.py:574 +#: amelie/calendar/models.py:193 amelie/files/models.py:149 +#: amelie/personal_tab/forms.py:479 amelie/tools/pdf.py:574 msgid "Attachments" msgstr "Bijlagen" -#: amelie/calendar/models.py:120 +#: amelie/calendar/models.py:196 msgid "Callback URL" msgstr "Callback URL" -#: amelie/calendar/models.py:121 +#: amelie/calendar/models.py:197 msgid "URL has to start with https://" msgstr "URL moet met https:// beginnen" -#: amelie/calendar/models.py:125 +#: amelie/calendar/models.py:201 msgid "Whether this event is cancelled, only cancel when needed. Participants will receive an email notification!" msgstr "Of dit evenement geannuleerd is, annuleer deze alleen wanneer nodig is. Deelnemers ontvangen hiervan een email!" -#: amelie/calendar/models.py:196 amelie/education/models.py:160 +#: amelie/calendar/models.py:272 amelie/education/models.py:160 #: amelie/room_duty/models.py:122 msgid "Start may not be after of simultaneous to end." msgstr "Begin kan niet na het einde voorkomen of gelijk zijn aan het einde." @@ -3532,14 +4196,6 @@ msgstr "" "\n" "Weet je zeker dat je een contante betaling van %(person)s voor %(event)s wil registreren?" -#: amelie/calendar/views.py:27 -msgid "Payment registered on {timezone.now().strftime('%d-%m-%Y')} by {request.user.person.incomplete_name()}" -msgstr "Betaling geregistreerd op {timezone.now().strftime('%d-%m-%Y')} door {request.user.person.incomplete_name()}" - -#: amelie/calendar/views.py:30 -msgid "Cash payment registered!" -msgstr "Contante betaling geregistreerd!" - #: amelie/claudia/account_views.py:61 amelie/claudia/account_views.py:74 msgid "Enter a correct username and password. Warning: the fields are case-sensitive." msgstr "Voer een correcte gebruikersnaam en wachtwoord in. Let op, de velden zijn hoofdletter-gevoelig." @@ -3672,7 +4328,7 @@ msgstr "Types" #: amelie/claudia/forms.py:27 amelie/claudia/forms.py:64 #: amelie/claudia/templates/claudia/extragroup_list.html:23 #: amelie/claudia/templates/claudia/mapping_detail.html:49 -#: amelie/members/models.py:348 +#: amelie/members/models.py:350 #: amelie/members/templates/includes/person_data_table.html:98 msgid "Account name" msgstr "Accountnaam" @@ -3726,7 +4382,7 @@ msgstr "De wachtwoordvelden zullen niet worden gebruikt, maar er zal een willeke msgid "This field is required." msgstr "Dit veld is verplicht." -#: amelie/claudia/models.py:33 amelie/members/models.py:1145 +#: amelie/claudia/models.py:33 amelie/members/models.py:1223 msgid "Create GitLab group" msgstr "GitLab groep aanmaken" @@ -3734,7 +4390,7 @@ msgstr "GitLab groep aanmaken" msgid "Members of this group get access to GitLab" msgstr "Leden van deze groep krijgen toegang tot GitLab" -#: amelie/claudia/models.py:35 amelie/members/models.py:1147 +#: amelie/claudia/models.py:35 amelie/members/models.py:1225 msgid "Create Matrix space" msgstr "Matrix-ruimte aanmaken" @@ -3748,7 +4404,7 @@ msgstr "Als een mail adres voor een extra groep is ingesteld, dan mag deze allee #: amelie/claudia/models.py:52 amelie/claudia/models.py:165 #: amelie/claudia/models.py:212 amelie/claudia/models.py:246 -#: amelie/members/models.py:183 amelie/members/models.py:1180 +#: amelie/members/models.py:185 amelie/members/models.py:1258 msgid "This email address is already in use by another mapping!" msgstr "Dit e-mailadres wordt al door een andere mapping gebruikt!" @@ -4046,11 +4702,6 @@ msgstr "Heb je nog vragen? Op https://wiki.ia.utwente.nl/ vind je onder het kopj msgid "Active member account at Inter-Actief" msgstr "Actieveledenaccount bij Inter-Actief" -#: amelie/claudia/templates/claudia/account_deleted.mail:5 -#, python-format -msgid "Dear %(name)s," -msgstr "Beste %(name)s," - #: amelie/claudia/templates/claudia/account_deleted.mail:7 msgid "" "According to the administration of Inter-Actief, you are no longer committee member and therefore should not have access to an active members account. This is the account with which you could log in to the computers and the Google Suite of Inter-Actief, your association membership will of course continue as normal.\n" @@ -4157,8 +4808,8 @@ msgstr "Aliasgroep aanmaken" #: amelie/claudia/templates/claudia/extraperson_list.html:21 #: amelie/claudia/templates/claudia/extrapersonalalias_list.html:29 #: amelie/claudia/templates/claudia/mapping_detail.html:26 -#: amelie/members/models.py:330 amelie/members/models.py:1130 -#: amelie/members/models.py:1329 amelie/members/query_forms.py:37 +#: amelie/members/models.py:332 amelie/members/models.py:1208 +#: amelie/members/models.py:1407 amelie/members/query_forms.py:37 #: amelie/members/templates/includes/committee_data.html:31 #: amelie/members/templates/includes/person_data_table.html:139 #: amelie/members/templates/includes/registration/data_overview_employee.html:14 @@ -4185,8 +4836,8 @@ msgstr "E-mailadres" #: amelie/companies/templates/companies/company_banners.html:64 #: amelie/companies/templates/companies/company_banners.html:100 #: amelie/companies/templates/companies/company_form.html:50 -#: amelie/members/models.py:102 amelie/members/models.py:824 -#: amelie/personal_tab/forms.py:135 +#: amelie/members/models.py:104 amelie/members/models.py:851 +#: amelie/personal_tab/forms.py:167 #: templates/health_check/partial_celery_tables.html:10 msgid "Active" msgstr "Actief" @@ -4242,9 +4893,9 @@ msgstr "Het e-mailadres %(email)s wordt gebruikt door de volgende mappings:" #: amelie/claudia/templates/claudia/mapping_list.html:50 #: amelie/claudia/templates/claudia/mapping_timeline.html:20 #: amelie/claudia/templates/claudia/timeline_list.html:19 -#: amelie/members/forms.py:296 amelie/members/models.py:68 -#: amelie/members/models.py:93 amelie/members/models.py:984 -#: amelie/personal_tab/forms.py:141 +#: amelie/members/forms.py:291 amelie/members/models.py:70 +#: amelie/members/models.py:95 amelie/members/models.py:995 +#: amelie/personal_tab/forms.py:173 msgid "Type" msgstr "Type" @@ -4281,8 +4932,8 @@ msgid "Create extra group" msgstr "Extra groep aanmaken" #: amelie/claudia/templates/claudia/extragroup_list.html:26 -#: amelie/members/models.py:149 amelie/members/models.py:901 -#: amelie/members/models.py:1361 +#: amelie/members/models.py:151 amelie/members/models.py:912 +#: amelie/members/models.py:1439 #: amelie/members/templates/includes/query/query_dogroup.html:4 #: amelie/members/templates/includes/registration/data_overview_freshmen.html:77 #: amelie/members/templates/includes/registration/study_freshmen.html:53 @@ -4633,7 +5284,7 @@ msgstr "Timeline" #: amelie/claudia/templates/claudia/mapping_detail.html:101 #: amelie/claudia/templates/claudia/mapping_detail.html:156 -#: amelie/members/models.py:331 amelie/members/models.py:1330 +#: amelie/members/models.py:333 amelie/members/models.py:1408 #: amelie/members/templates/includes/person_data_table.html:113 #: amelie/members/templates/includes/registration/contact_member.html:11 #: amelie/members/templates/includes/registration/data_overview_employee.html:25 @@ -4669,7 +5320,7 @@ msgstr "automatisch" #: amelie/claudia/templates/claudia/mapping_detail.html:167 #: amelie/claudia/templates/claudia/mapping_detail.html:168 #: amelie/claudia/templates/claudia/mapping_detail.html:188 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:78 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:83 msgid "yes" msgstr "ja" @@ -4715,7 +5366,7 @@ msgstr "Mapping:" #: amelie/claudia/templates/claudia/mapping_timeline.html:19 #: amelie/claudia/templates/claudia/timeline_list.html:17 -#: amelie/personal_tab/templates/lists/activity_transactions.html:8 +#: amelie/personal_tab/templates/lists/activity_transactions.html:9 msgid "Date/time" msgstr "Datum/tijd" @@ -4794,11 +5445,11 @@ msgstr "Company Corner bedrijf of naam/website verplicht." msgid "Activity's \"visible from\" date is later than its \"visible until\" date" msgstr "Activiteit zichtbaarheid \"vanaf\" datum is later dan de \"tot\" datum" -#: amelie/companies/forms.py:139 amelie/personal_tab/forms.py:182 +#: amelie/companies/forms.py:139 amelie/personal_tab/forms.py:214 msgid "Beginning:" msgstr "Begin:" -#: amelie/companies/forms.py:140 amelie/personal_tab/forms.py:183 +#: amelie/companies/forms.py:140 amelie/personal_tab/forms.py:215 msgid "End (till)" msgstr "Einde (tot):" @@ -4834,11 +5485,11 @@ msgstr "De absolute URL naar deze activiteit" msgid "Whether this event is visible" msgstr "Of deze activiteit zichtbaar is" -#: amelie/companies/models.py:18 amelie/personal_tab/models.py:430 +#: amelie/companies/models.py:18 amelie/personal_tab/models.py:466 msgid "name" msgstr "naam" -#: amelie/companies/models.py:22 amelie/personal_tab/models.py:433 +#: amelie/companies/models.py:22 amelie/personal_tab/models.py:469 msgid "name (en)" msgstr "naam (en)" @@ -4959,7 +5610,7 @@ msgstr "" #: amelie/companies/templates/companies/company_banners.html:17 #: amelie/companies/templates/companies/company_banners.html:53 #: amelie/companies/templates/companies/company_banners.html:89 -#: amelie/personal_tab/models.py:755 +#: amelie/personal_tab/models.py:791 msgid "New" msgstr "Nieuw" @@ -5067,7 +5718,7 @@ msgid "iCal all external events" msgstr "iCal alle externe activiteiten" #: amelie/companies/templates/companies/company_event_list.html:19 -#: amelie/members/models.py:850 +#: amelie/personal_tab/models.py:960 msgid "Visible" msgstr "Zichtbaar" @@ -5173,7 +5824,7 @@ msgstr "Selecteer een periode en klik op Statistieken." #: amelie/companies/templates/companies/company_statistics.html:23 #: amelie/personal_tab/templates/cookie_corner_overview.html:19 -#: amelie/personal_tab/templates/statistics/statistics.html:452 +#: amelie/personal_tab/templates/statistics/statistics.html:450 #: amelie/personal_tab/templates/statistics/statistics_form.html:21 #: amelie/room_duty/templates/room_duty/table/overview.html:153 #: amelie/statistics/templates/statistics.html:5 @@ -5569,8 +6220,8 @@ msgstr "Module" #: amelie/education/forms.py:51 amelie/education/forms.py:58 #: amelie/education/forms.py:94 amelie/education/models.py:221 #: amelie/education/templates/complaints.html:59 -#: amelie/education/templates/complaints.html:119 amelie/members/models.py:151 -#: amelie/members/models.py:897 +#: amelie/education/templates/complaints.html:119 amelie/members/models.py:153 +#: amelie/members/models.py:908 #: amelie/members/templates/includes/query/query_study.html:4 #: amelie/members/templates/members/datablock_html.mail:13 #: amelie/members/templates/members/datablock_plain.mail:12 @@ -5582,9 +6233,9 @@ msgid "Course" msgstr "Vak" #: amelie/education/forms.py:61 amelie/education/models.py:132 -#: amelie/education/models.py:265 amelie/members/models.py:985 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:18 -#: amelie/personal_tab/templates/lists/transactions_totals.html:18 +#: amelie/education/models.py:265 amelie/members/models.py:996 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:19 +#: amelie/personal_tab/templates/lists/transactions_totals.html:19 #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:17 #: amelie/personal_tab/templates/unpaid_memberships/overview.html:13 msgid "Year" @@ -5606,8 +6257,8 @@ msgstr "Docent" #: amelie/education/forms.py:95 #: amelie/education/templates/dea_nomination.html:25 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:162 -#: amelie/personal_tab/templates/transactions/reversal_transaction_detail.html:23 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:167 +#: amelie/personal_tab/templates/info/transaction_information.html:117 #: amelie/tools/templates/data_export_statistics.html:28 msgid "Reason" msgstr "Reden" @@ -5665,14 +6316,14 @@ msgstr "Inhoud van de pagina (gelokaliseerd voor gebruiker)" msgid "Category name (localized for user)" msgstr "Categorienaam (gelokaliseerd voor gebruiker)" -#: amelie/education/models.py:31 amelie/members/forms.py:630 -#: amelie/members/models.py:1123 amelie/personal_tab/models.py:293 -#: amelie/personal_tab/models.py:350 +#: amelie/education/models.py:31 amelie/members/forms.py:633 +#: amelie/members/models.py:1201 amelie/personal_tab/models.py:329 +#: amelie/personal_tab/models.py:386 #: amelie/personal_tab/templates/statistics/statistics.html:54 msgid "Category" msgstr "Categorie" -#: amelie/education/models.py:32 amelie/personal_tab/models.py:351 +#: amelie/education/models.py:32 amelie/personal_tab/models.py:387 msgid "Categories" msgstr "Categorieën" @@ -5733,7 +6384,7 @@ msgid "Modules" msgstr "Modules" #: amelie/education/models.py:222 amelie/education/templates/summaries.html:22 -#: amelie/members/models.py:227 +#: amelie/members/models.py:229 msgid "Courses" msgstr "Vakken" @@ -6580,7 +7231,7 @@ msgstr "Vakcode van de module" msgid "Education" msgstr "Onderwijs" -#: amelie/education/templates/overview.html:12 amelie/members/models.py:1134 +#: amelie/education/templates/overview.html:12 amelie/members/models.py:1212 #: templates/basis.html:272 msgid "Information" msgstr "Informatie" @@ -6785,8 +7436,14 @@ msgstr "" "Er zijn %(mail_count)s berichten verstuurd. Hiervan zijn er %(sent_count)s geaccepteerd\n" "en %(error_count)s niet geaccepteerd." -#: amelie/members/forms.py:36 amelie/members/models.py:322 -#: amelie/members/models.py:1323 +#: amelie/members/ajax_views.py:220 +#, fuzzy, python-brace-format +#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgid "Early termination of membership {membership_type} ({begin_year}/{end_year})" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/members/forms.py:36 amelie/members/models.py:324 +#: amelie/members/models.py:1401 #: amelie/members/templates/includes/person_data_table.html:82 #: amelie/members/templates/includes/registration/data_overview_employee.html:11 #: amelie/members/templates/includes/registration/data_overview_external.html:11 @@ -6800,8 +7457,8 @@ msgstr "" msgid "Gender" msgstr "Geslacht" -#: amelie/members/forms.py:37 amelie/members/models.py:324 -#: amelie/members/models.py:1325 +#: amelie/members/forms.py:37 amelie/members/models.py:326 +#: amelie/members/models.py:1403 #: amelie/members/templates/includes/person_data_table.html:88 #: amelie/members/templates/includes/registration/data_overview_employee.html:17 #: amelie/members/templates/includes/registration/data_overview_external.html:17 @@ -6815,15 +7472,15 @@ msgstr "Geslacht" msgid "Language of preference" msgstr "Voorkeurstaal" -#: amelie/members/forms.py:39 amelie/members/models.py:326 -#: amelie/members/models.py:1327 +#: amelie/members/forms.py:39 amelie/members/models.py:328 +#: amelie/members/models.py:1405 #: amelie/members/templates/includes/person_data_table.html:85 #: amelie/members/templates/includes/registration/personal_details.html:93 msgid "International student" msgstr "Internationale student" -#: amelie/members/forms.py:40 amelie/members/models.py:328 -#: amelie/members/models.py:1328 +#: amelie/members/forms.py:40 amelie/members/models.py:330 +#: amelie/members/models.py:1406 #: amelie/members/templates/includes/person_data_table.html:78 #: amelie/members/templates/includes/registration/data_overview_employee.html:8 #: amelie/members/templates/includes/registration/data_overview_external.html:8 @@ -6837,8 +7494,8 @@ msgstr "Internationale student" msgid "Birth date" msgstr "Geboortedatum" -#: amelie/members/forms.py:42 amelie/members/models.py:373 -#: amelie/members/models.py:1346 +#: amelie/members/forms.py:42 amelie/members/models.py:375 +#: amelie/members/models.py:1424 #: amelie/members/templates/includes/query/query_preference.html:4 #: amelie/members/templates/members/datablock_html.mail:24 #: amelie/members/templates/members/datablock_plain.mail:22 @@ -6887,122 +7544,122 @@ msgstr "Suggestie:" msgid "Related to" msgstr "Verbonden aan" -#: amelie/members/forms.py:304 +#: amelie/members/forms.py:299 msgid "Current association year" msgstr "Huidig verenigingsjaar" -#: amelie/members/forms.py:305 +#: amelie/members/forms.py:300 msgid "Upcoming association year" msgstr "Volgend verenigingsjaar" -#: amelie/members/forms.py:311 +#: amelie/members/forms.py:313 msgid "Does the member still have to pay the membership fee?" msgstr "Moet het lid de contributie nog wel betalen?" -#: amelie/members/forms.py:323 +#: amelie/members/forms.py:325 msgid "Immediately" msgstr "Per direct" -#: amelie/members/forms.py:325 +#: amelie/members/forms.py:327 msgid "At the end of the association year" msgstr "Aan het einde van het verenigingsjaar" -#: amelie/members/forms.py:328 +#: amelie/members/forms.py:331 msgid "Send the signature request" msgstr "Verstuur het ondertekeningsverzoek" -#: amelie/members/forms.py:334 +#: amelie/members/forms.py:337 msgid "Send a signature request" msgstr "Verstuur ondertekeningsverzoek" -#: amelie/members/forms.py:335 +#: amelie/members/forms.py:338 msgid "This will send an e-mail to the member, with a request to sign the mandate form via our signing tool." msgstr "Dit zal een e-mail naar het lid toe sturen met het verzoek om de machtiging te ondertekenen via onze ondertekeningstool." -#: amelie/members/forms.py:354 +#: amelie/members/forms.py:357 msgid "A mandate type is required." msgstr "Machtigingssoort is verplicht." -#: amelie/members/forms.py:357 amelie/members/forms.py:668 +#: amelie/members/forms.py:360 amelie/members/forms.py:671 msgid "An IBAN is required." msgstr "IBAN is verplicht." -#: amelie/members/forms.py:376 amelie/personal_tab/forms.py:193 +#: amelie/members/forms.py:379 amelie/personal_tab/forms.py:225 msgid "Choose a name from the suggestions." msgstr "Kies een naam uit de suggesties." -#: amelie/members/forms.py:437 +#: amelie/members/forms.py:440 msgid "If we can use your parent(s)/guardian(s) data, then you need to enter at least their e-mail address!" msgstr "Als we de gegevens van je ouder(s)/verzorger(s) mogen gebruiken dan moet je in ieder geval het e-mailadres invullen!" -#: amelie/members/forms.py:442 +#: amelie/members/forms.py:445 msgid "If we can not use your parent(s)/guardian(s) data, then you do not need to enter any data!" msgstr "Als we de gegevens van je ouder(s)/verzorger(s) niet mogen gebruiken dan moet je ook geen gegevens invullen!" -#: amelie/members/forms.py:469 +#: amelie/members/forms.py:472 msgid "A study has been chosen, but no cohort was specified. Enter the missing data to continue." msgstr "Er is wel een studie gekozen, maar geen generatie. Vul een generatie in om verder te gaan." -#: amelie/members/forms.py:476 +#: amelie/members/forms.py:479 msgid "A cohort has been chosen, but no study was specified. Enter at least one study to continue." msgstr "Er is wel een generatie gekozen, maar geen studie. Vul een studie in om verder te gaan." -#: amelie/members/forms.py:483 amelie/members/forms.py:505 +#: amelie/members/forms.py:486 amelie/members/forms.py:508 msgid "A student with this student number already exists." msgstr "Student met dit studentnummer bestaat al." -#: amelie/members/forms.py:485 amelie/members/forms.py:507 +#: amelie/members/forms.py:488 amelie/members/forms.py:510 msgid "This student number is already pre-enrolled. A board member can activate your account." msgstr "Dit studentnummer is al vooringeschreven. Een bestuurder kan je account activeren." -#: amelie/members/forms.py:516 +#: amelie/members/forms.py:519 msgid "An employee with this employee number already exists." msgstr "Medewerker met dit medewerkersnummer bestaat al." -#: amelie/members/forms.py:541 amelie/members/query_forms.py:85 +#: amelie/members/forms.py:544 amelie/members/query_forms.py:85 #: amelie/members/templates/includes/registration/authorizations.html:53 #: amelie/members/templates/includes/registration/data_overview_employee.html:62 #: amelie/members/templates/includes/registration/data_overview_external.html:62 #: amelie/members/templates/includes/registration/data_overview_freshmen.html:94 #: amelie/members/templates/includes/registration/data_overview_general.html:71 -#: amelie/personal_tab/forms.py:112 amelie/personal_tab/forms.py:143 -#: amelie/personal_tab/forms.py:417 amelie/personal_tab/forms.py:422 -#: amelie/personal_tab/models.py:503 amelie/personal_tab/models.py:1028 -#: amelie/personal_tab/templates/authorization/view.html:33 +#: amelie/personal_tab/forms.py:110 amelie/personal_tab/forms.py:175 +#: amelie/personal_tab/forms.py:455 amelie/personal_tab/forms.py:460 +#: amelie/personal_tab/models.py:539 amelie/personal_tab/models.py:1440 +#: amelie/personal_tab/templates/authorization/view.html:38 #: amelie/tools/pdf.py:412 amelie/tools/pdf.py:550 msgid "IBAN" msgstr "IBAN" -#: amelie/members/forms.py:542 +#: amelie/members/forms.py:545 #: amelie/members/templates/includes/registration/authorizations.html:66 #: amelie/members/templates/includes/registration/data_overview_employee.html:65 #: amelie/members/templates/includes/registration/data_overview_external.html:65 #: amelie/members/templates/includes/registration/data_overview_freshmen.html:97 #: amelie/members/templates/includes/registration/data_overview_general.html:74 -#: amelie/personal_tab/forms.py:115 amelie/personal_tab/forms.py:144 -#: amelie/personal_tab/models.py:506 -#: amelie/personal_tab/templates/authorization/view.html:37 +#: amelie/personal_tab/forms.py:113 amelie/personal_tab/forms.py:176 +#: amelie/personal_tab/models.py:542 amelie/personal_tab/models.py:1329 +#: amelie/personal_tab/templates/authorization/view.html:42 #: amelie/tools/pdf.py:414 msgid "BIC" msgstr "BIC" -#: amelie/members/forms.py:583 +#: amelie/members/forms.py:586 msgid "By last name" msgstr "Op achternaam" -#: amelie/members/forms.py:584 +#: amelie/members/forms.py:587 msgid "By first name" msgstr "Op voornaam" -#: amelie/members/forms.py:585 +#: amelie/members/forms.py:588 msgid "By student number" msgstr "Op studentnummer" -#: amelie/members/forms.py:586 +#: amelie/members/forms.py:589 msgid "By order of pre-enrollment" msgstr "Op volgorde van voorinschrijving" -#: amelie/members/forms.py:596 amelie/members/models.py:1288 +#: amelie/members/forms.py:599 amelie/members/models.py:1366 #: amelie/members/templates/committee_edit_members.html:34 #: amelie/members/templates/committee_edit_single_member.html:29 #: amelie/members/templates/committee_members.html:8 @@ -7011,13 +7668,13 @@ msgstr "Op volgorde van voorinschrijving" msgid "Position" msgstr "Functie" -#: amelie/members/forms.py:632 +#: amelie/members/forms.py:635 #: amelie/members/templates/includes/committee_data.html:20 msgid "Parent committee" msgstr "Oudercommissie" -#: amelie/members/forms.py:651 amelie/members/models.py:868 -#: amelie/members/models.py:1357 +#: amelie/members/forms.py:654 amelie/members/models.py:879 +#: amelie/members/models.py:1435 #: amelie/members/templates/includes/registration/data_overview_freshmen.html:74 #: amelie/members/templates/includes/registration/data_overview_general.html:51 #: amelie/members/templates/includes/registration/study_freshmen.html:39 @@ -7029,34 +7686,34 @@ msgstr "Oudercommissie" msgid "Student number" msgstr "Studentnummer" -#: amelie/members/forms.py:659 +#: amelie/members/forms.py:662 msgid "There is no student with this student number." msgstr "Student met dit studentnummer bestaat niet." -#: amelie/members/forms.py:672 +#: amelie/members/forms.py:675 msgid "Please enter your own bank account number!" msgstr "Vul alstublieft uw eigen rekeningnummer in!" -#: amelie/members/forms.py:676 +#: amelie/members/forms.py:679 msgid "BIC has to be entered for foreign bankaccounts." msgstr "BIC moet ingevuld worden met een buitenlandse rekening." -#: amelie/members/forms.py:682 +#: amelie/members/forms.py:683 msgid "BIC could not be generated, please enter it yourself." msgstr "Kon BIC niet automatisch bepalen, vul deze alsjeblieft zelf in." -#: amelie/members/forms.py:685 +#: amelie/members/forms.py:687 amelie/members/forms.py:720 +msgid "The bank associated with your BIC does not accept SEPA Direct Debits." +msgstr "" + +#: amelie/members/forms.py:690 msgid "The BIC country does not match the IBAN country." msgstr "Het land in de BIC komt niet overeen met het land in het IBAN." -#: amelie/members/forms.py:690 amelie/members/forms.py:696 +#: amelie/members/forms.py:695 amelie/members/forms.py:701 msgid "The BIC bank does not match the IBAN bank." msgstr "De bank in de BIC komt niet overeen met de bank in het IBAN." -#: amelie/members/forms.py:715 -msgid "BIC is not from a SEPA country" -msgstr "BIC komt niet uit een SEPA land" - #: amelie/members/graphql.py:25 msgid "Person name" msgstr "Naam van persoon" @@ -7081,217 +7738,242 @@ msgstr "Commissie-informatie (gelokaliseerd voor gebruiker)" msgid "Committees in this category" msgstr "Commissies in deze categorie" -#: amelie/members/models.py:45 amelie/members/models.py:66 -#: amelie/members/models.py:91 amelie/members/models.py:1119 +#: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:162 +#: amelie/members/models.py:1074 +#, python-brace-format +msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:202 +#, fuzzy, python-brace-format +#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgid "Internal settlement for reversed contribution {membership_type} ({begin_year}/{end_year}) (migrated)" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:234 +#, fuzzy, python-brace-format +#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgid "Manual payment for contribution {membership_type} ({begin_year}/{end_year}) (migrated)" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:266 +#, fuzzy, python-brace-format +#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgid "Pre-SEPA payment for contribution {membership_type} ({begin_year}/{end_year}) (migrated)" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/members/models.py:47 amelie/members/models.py:68 +#: amelie/members/models.py:93 amelie/members/models.py:1197 msgid "Abbreviation" msgstr "Afkorting" -#: amelie/members/models.py:49 +#: amelie/members/models.py:51 msgid "faculty" msgstr "faculteit" -#: amelie/members/models.py:50 +#: amelie/members/models.py:52 msgid "faculties" msgstr "faculteiten" -#: amelie/members/models.py:62 +#: amelie/members/models.py:64 msgid "Chair" msgstr "Leerstoel" -#: amelie/members/models.py:63 templates/health_check/index.html:32 +#: amelie/members/models.py:65 templates/health_check/index.html:32 msgid "Service" msgstr "Dienst" -#: amelie/members/models.py:67 +#: amelie/members/models.py:69 msgid "Faculty" msgstr "Faculteit" -#: amelie/members/models.py:72 +#: amelie/members/models.py:74 msgid "department" msgstr "afdeling" -#: amelie/members/models.py:73 +#: amelie/members/models.py:75 msgid "departments" msgstr "afdelingen" -#: amelie/members/models.py:85 +#: amelie/members/models.py:87 msgid "Bachelor of Science" msgstr "Bachelor of Science" -#: amelie/members/models.py:86 +#: amelie/members/models.py:88 msgid "Master of Science" msgstr "Master of Science" -#: amelie/members/models.py:87 +#: amelie/members/models.py:89 msgid "Engineer" msgstr "Ingenieur" -#: amelie/members/models.py:92 +#: amelie/members/models.py:94 msgid "Faculties" msgstr "Faculteiten" -#: amelie/members/models.py:94 +#: amelie/members/models.py:96 msgid "Course length" msgstr "Studielengte" -#: amelie/members/models.py:97 +#: amelie/members/models.py:99 msgid "Primary study" msgstr "Hoofdstudie" -#: amelie/members/models.py:98 +#: amelie/members/models.py:100 msgid "Indicates that this study is one of our primary studies" msgstr "Geeft aan dat deze studie een van onze primaire studies is" -#: amelie/members/models.py:103 +#: amelie/members/models.py:105 msgid "Make a study inactive when the study is not given any more on the University of Twente" msgstr "Maak een studie inactief als de studie niet meer gegeven wordt op de Universiteit Twente" -#: amelie/members/models.py:117 +#: amelie/members/models.py:119 msgid "study" msgstr "studie" -#: amelie/members/models.py:118 +#: amelie/members/models.py:120 msgid "studies" msgstr "studies" -#: amelie/members/models.py:130 +#: amelie/members/models.py:132 msgid "Color" msgstr "Kleur" -#: amelie/members/models.py:130 +#: amelie/members/models.py:132 msgid "What is the color of this dogroup?" msgstr "Wat is de kleur van deze doegroep?" -#: amelie/members/models.py:135 +#: amelie/members/models.py:137 msgid "do-group" msgstr "doegroep" -#: amelie/members/models.py:136 +#: amelie/members/models.py:138 msgid "do-groups" msgstr "doegroepen" -#: amelie/members/models.py:148 +#: amelie/members/models.py:150 msgid "Generation" msgstr "Generatie" -#: amelie/members/models.py:150 +#: amelie/members/models.py:152 msgid "Introduction parents" msgstr "Doegroepouders" -#: amelie/members/models.py:152 +#: amelie/members/models.py:154 msgid "Mailalias" msgstr "Mailalias" -#: amelie/members/models.py:153 +#: amelie/members/models.py:155 msgid "Generation color" msgstr "Generatiekleur" -#: amelie/members/models.py:154 +#: amelie/members/models.py:156 msgid "Similar to the dogroup color, however this value can be set in order to override the color for just this generation" msgstr "Vergelijkbaar met de kleur van een doegroep, maar als deze is ingevuld overschrijft het de originele doegroep kleurwaarde" -#: amelie/members/models.py:158 +#: amelie/members/models.py:160 msgid "do-group generation" msgstr "doegroepgeneratie" -#: amelie/members/models.py:159 +#: amelie/members/models.py:161 msgid "do-group generations" msgstr "doegroepgeneraties" -#: amelie/members/models.py:172 +#: amelie/members/models.py:174 msgid "The mail alias for a dogroup generation may only point to an Inter-Actief server." msgstr "Het email alias voor een doegroep mag alleen maar verwijzen naar een Inter-Actief server." -#: amelie/members/models.py:231 +#: amelie/members/models.py:233 msgid "association" msgstr "vereniging" -#: amelie/members/models.py:232 +#: amelie/members/models.py:234 msgid "associations" msgstr "verenigingen" -#: amelie/members/models.py:242 +#: amelie/members/models.py:244 msgid "Category of preference" msgstr "Voorkeurscategorie" -#: amelie/members/models.py:245 +#: amelie/members/models.py:247 msgid "category of preference" msgstr "voorkeurscategorie" -#: amelie/members/models.py:246 +#: amelie/members/models.py:248 msgid "categories of preference" msgstr "voorkeurscategorieën" -#: amelie/members/models.py:257 +#: amelie/members/models.py:259 msgid "Preference" msgstr "Voorkeur" -#: amelie/members/models.py:258 +#: amelie/members/models.py:260 msgid "Preference(s)" msgstr "Voorkeur(en)" -#: amelie/members/models.py:260 +#: amelie/members/models.py:262 msgid "Standard" msgstr "Standaard" -#: amelie/members/models.py:261 +#: amelie/members/models.py:263 msgid "Adjustable by the user in profile" msgstr "Instelbaar door gebruiker in profiel" -#: amelie/members/models.py:262 +#: amelie/members/models.py:264 msgid "Adjustable by the user on login" msgstr "Instelbaar door gebruiker bij inloggen" -#: amelie/members/models.py:263 amelie/members/templates/person_mandate.html:82 +#: amelie/members/models.py:265 amelie/members/templates/person_mandate.html:82 #: amelie/room_duty/templates/room_duty/table/overview.html:90 msgid "Print" msgstr "Print" -#: amelie/members/models.py:276 amelie/members/models.py:277 +#: amelie/members/models.py:278 amelie/members/models.py:279 msgid "preference" msgstr "voorkeur" -#: amelie/members/models.py:296 +#: amelie/members/models.py:298 msgid "Man" msgstr "Man" -#: amelie/members/models.py:297 +#: amelie/members/models.py:299 msgid "Woman" msgstr "Vrouw" -#: amelie/members/models.py:298 -#: amelie/personal_tab/templates/cookie_corner_transactions.html:48 -#: amelie/personal_tab/templates/lists/transactions_totals.html:24 +#: amelie/members/models.py:300 +#: amelie/personal_tab/templates/cookie_corner_transactions.html:51 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:73 +#: amelie/personal_tab/templates/lists/transactions_totals.html:26 #: amelie/personal_tab/templates/statistics/statistics.html:313 #: templates/frontpage.html:139 msgid "Other" msgstr "Overig" -#: amelie/members/models.py:301 +#: amelie/members/models.py:303 msgid "Yes, I am an international student." msgstr "Ja, ik ben een internationale student." -#: amelie/members/models.py:302 +#: amelie/members/models.py:304 msgid "No, I am not an international student." msgstr "Nee, ik ben geen internationale student." -#: amelie/members/models.py:303 +#: amelie/members/models.py:305 msgid "I would rather not say if I'm an international student or not." msgstr "Ik wil liever niet zeggen of ik een internationale student ben of niet." -#: amelie/members/models.py:306 +#: amelie/members/models.py:308 msgid "No preference" msgstr "Geen voorkeur" -#: amelie/members/models.py:307 +#: amelie/members/models.py:309 msgid "Bash" msgstr "Bash" -#: amelie/members/models.py:308 +#: amelie/members/models.py:310 msgid "Z shell" msgstr "Z shell" -#: amelie/members/models.py:313 amelie/members/models.py:1322 +#: amelie/members/models.py:315 amelie/members/models.py:1400 #: amelie/members/templates/includes/registration/personal_details.html:29 #: amelie/members/templates/includes/registration/personal_details_employee.html:29 #: amelie/members/templates/members/datablock_html.mail:3 @@ -7299,21 +7981,21 @@ msgstr "Z shell" msgid "Initials" msgstr "Initialen" -#: amelie/members/models.py:317 +#: amelie/members/models.py:319 #: amelie/members/templates/includes/query/query_results.html:61 msgid "Photo" msgstr "Foto" -#: amelie/members/models.py:318 +#: amelie/members/models.py:320 msgid "Unverified photo" msgstr "Onbeoordeelde foto" -#: amelie/members/models.py:320 +#: amelie/members/models.py:322 #: amelie/members/templates/includes/query/query_results.html:64 msgid "Notes" msgstr "Notities" -#: amelie/members/models.py:332 amelie/members/models.py:1331 +#: amelie/members/models.py:334 amelie/members/models.py:1409 #: amelie/members/templates/includes/registration/contact_member.html:24 #: amelie/members/templates/includes/registration/data_overview_employee.html:28 #: amelie/members/templates/includes/registration/data_overview_external.html:31 @@ -7326,7 +8008,7 @@ msgstr "Notities" msgid "Postal code" msgstr "Postcode" -#: amelie/members/models.py:333 amelie/members/models.py:1332 +#: amelie/members/models.py:335 amelie/members/models.py:1410 #: amelie/members/templates/includes/registration/contact_member.html:37 #: amelie/members/templates/includes/registration/data_overview_employee.html:31 #: amelie/members/templates/includes/registration/data_overview_external.html:34 @@ -7339,7 +8021,7 @@ msgstr "Postcode" msgid "City" msgstr "Plaats" -#: amelie/members/models.py:334 amelie/members/models.py:1333 +#: amelie/members/models.py:336 amelie/members/models.py:1411 #: amelie/members/templates/includes/registration/contact_member.html:50 #: amelie/members/templates/includes/registration/data_overview_employee.html:34 #: amelie/members/templates/includes/registration/data_overview_external.html:37 @@ -7352,7 +8034,7 @@ msgstr "Plaats" msgid "Country" msgstr "Land" -#: amelie/members/models.py:335 amelie/members/models.py:1334 +#: amelie/members/models.py:337 amelie/members/models.py:1412 #: amelie/members/query_forms.py:36 #: amelie/members/templates/members/datablock_html.mail:20 #: amelie/members/templates/members/datablock_plain.mail:19 @@ -7360,357 +8042,346 @@ msgstr "Land" msgid "Phonenumber" msgstr "Telefoonnummer" -#: amelie/members/models.py:337 amelie/members/models.py:1336 +#: amelie/members/models.py:339 amelie/members/models.py:1414 #: amelie/members/templates/includes/person_data_table.html:164 #: amelie/members/templates/includes/registration/contact_parents.html:25 msgid "E-mail address of parent(s)/guardian(s)" msgstr "E-mailadres van ouder(s)/verzorger(s)" -#: amelie/members/models.py:338 amelie/members/models.py:1337 +#: amelie/members/models.py:340 amelie/members/models.py:1415 #: amelie/members/templates/includes/person_data_table.html:147 #: amelie/members/templates/includes/registration/contact_parents.html:38 #: amelie/tools/pdf.py:131 templates/profile_edit.html:57 msgid "Address of parent(s)/guardian(s)" msgstr "Adres van ouder(s)/verzorger(s)" -#: amelie/members/models.py:340 amelie/members/models.py:1339 +#: amelie/members/models.py:342 amelie/members/models.py:1417 #: amelie/members/templates/includes/registration/contact_parents.html:51 msgid "Postal code of parent(s)/guardian(s)" msgstr "Postcode van ouder(s)/verzorger(s)" -#: amelie/members/models.py:342 amelie/members/models.py:1341 +#: amelie/members/models.py:344 amelie/members/models.py:1419 #: amelie/members/templates/includes/registration/contact_parents.html:64 #: amelie/tools/pdf.py:133 msgid "Residence of parent(s)/guardian(s)" msgstr "Woonplaats van ouder(s)/verzorger(s)" -#: amelie/members/models.py:344 amelie/members/models.py:1343 +#: amelie/members/models.py:346 amelie/members/models.py:1421 #: amelie/members/templates/includes/registration/contact_parents.html:77 #: amelie/tools/pdf.py:135 msgid "Country of parent(s)/guardian(s)" msgstr "Land van ouder(s)/verzorger(s)" -#: amelie/members/models.py:345 amelie/members/models.py:1344 +#: amelie/members/models.py:347 amelie/members/models.py:1422 #: amelie/members/templates/includes/registration/contact_parents.html:16 #: templates/profile_edit.html:64 msgid "My parents' address details may be used for the parents day." msgstr "De adresgegevens van mijn ouders mogen gebruikt worden voor de ouderdag." -#: amelie/members/models.py:349 +#: amelie/members/models.py:351 msgid "You can only enter ^[a-z]*$." msgstr "Je mag alleen ^[a-z]*$ invullen." -#: amelie/members/models.py:349 amelie/members/models.py:355 -#: amelie/members/models.py:1120 +#: amelie/members/models.py:351 amelie/members/models.py:357 +#: amelie/members/models.py:1198 msgid "Invalid account name" msgstr "Ongeldige accountnaam" -#: amelie/members/models.py:354 +#: amelie/members/models.py:356 msgid "UT external account name" msgstr "UT Externen-accountnaam" -#: amelie/members/models.py:355 +#: amelie/members/models.py:357 #, python-brace-format msgid "You can only enter ^x[0-9]{7}$." msgstr "Je mag alleen ^x[0-9]{7}$ invullen." -#: amelie/members/models.py:362 +#: amelie/members/models.py:364 msgid "Minecraft username" msgstr "Minecraft gebruikersnaam" -#: amelie/members/models.py:367 +#: amelie/members/models.py:369 msgid "Minecraft UUID" msgstr "Minecraft UUID" -#: amelie/members/models.py:369 +#: amelie/members/models.py:371 msgid "Unix shell" msgstr "UNIX shell" -#: amelie/members/models.py:370 +#: amelie/members/models.py:372 msgid "Is web master" msgstr "Is webmaster" -#: amelie/members/models.py:371 +#: amelie/members/models.py:373 msgid "Has signed NDA" msgstr "Heeft getekende NDA" -#: amelie/members/models.py:374 +#: amelie/members/models.py:376 msgid "User" msgstr "Gebruiker" -#: amelie/members/models.py:377 +#: amelie/members/models.py:379 msgid "Password reset code" msgstr "Wachtwoordherstelcode" -#: amelie/members/models.py:379 +#: amelie/members/models.py:381 msgid "Password reset code expiry" msgstr "Verloopdatum wachtwoordherstelcode" -#: amelie/members/models.py:382 +#: amelie/members/models.py:384 msgid "Sudo password reset code" msgstr "Sudo-wachtwoordherstelcode" -#: amelie/members/models.py:384 +#: amelie/members/models.py:386 msgid "Sudo password reset code expiry" msgstr "Verloopdatum sudowachtwoordherstelcode" -#: amelie/members/models.py:391 amelie/personal_tab/models.py:93 -#: amelie/personal_tab/models.py:500 amelie/personal_tab/models.py:1019 +#: amelie/members/models.py:393 amelie/personal_tab/models.py:98 +#: amelie/personal_tab/models.py:536 amelie/personal_tab/models.py:1040 +#: amelie/personal_tab/models.py:1431 msgid "person" msgstr "persoon" -#: amelie/members/models.py:392 +#: amelie/members/models.py:394 msgid "people" msgstr "personen" -#: amelie/members/models.py:723 +#: amelie/members/models.py:628 +#, fuzzy +#| msgid "Add transaction" +msgid "Invalid transaction type" +msgstr "Transactie toevoegen" + +#: amelie/members/models.py:750 msgid "Your email address is not to point to an Inter-Actief server. If you have a Google Apps account please use firstname.lastname@gapps.inter-actief.nl as an alternative." msgstr "Je E-mailadres mag niet naar een Inter-Actief server verwijzen. Als je een Google Apps account hebt kan je voornaam.achternaam@gapps.inter-actief.nl gebruiken." -#: amelie/members/models.py:769 +#: amelie/members/models.py:796 msgid "Documenso ID was not given. Can't do anything." msgstr "Documenso-ID is niet opgegeven. Ik kan hier niks doen." -#: amelie/members/models.py:822 +#: amelie/members/models.py:849 msgid "Needs verification" msgstr "Moet geverifiëerd worden" -#: amelie/members/models.py:838 +#: amelie/members/models.py:865 msgid "membership type" msgstr "lidmaatschapstype" -#: amelie/members/models.py:839 +#: amelie/members/models.py:866 msgid "membership types" msgstr "lidmaatschapstypes" -#: amelie/members/models.py:854 -msgid "way of payment" -msgstr "betalingswijze" - -#: amelie/members/models.py:855 -msgid "ways of payment" -msgstr "betalingswijzen" - -#: amelie/members/models.py:872 +#: amelie/members/models.py:883 msgid "student" msgstr "student" -#: amelie/members/models.py:873 +#: amelie/members/models.py:884 msgid "students" msgstr "studenten" -#: amelie/members/models.py:896 +#: amelie/members/models.py:907 msgid "Student" msgstr "Student" -#: amelie/members/models.py:898 +#: amelie/members/models.py:909 #: amelie/room_duty/templates/room_duty/table/fill.html:19 #: amelie/room_duty/templates/room_duty/table/overview.html:100 msgid "Begin" msgstr "Begin" -#: amelie/members/models.py:899 amelie/members/models.py:921 +#: amelie/members/models.py:910 amelie/members/models.py:932 #: amelie/room_duty/templates/room_duty/table/fill.html:20 #: amelie/room_duty/templates/room_duty/table/overview.html:101 msgid "End" msgstr "Eind" -#: amelie/members/models.py:900 +#: amelie/members/models.py:911 msgid "Has graduated" msgstr "Is afgestudeerd" -#: amelie/members/models.py:905 +#: amelie/members/models.py:916 msgid "study period" msgstr "studieperiode" -#: amelie/members/models.py:906 +#: amelie/members/models.py:917 msgid "study periods" msgstr "studieperiodes" -#: amelie/members/models.py:919 +#: amelie/members/models.py:930 #: amelie/members/templates/includes/registration/data_overview_employee.html:45 #: amelie/members/templates/includes/registration/employee_general.html:10 #: amelie/tools/pdf.py:141 amelie/tools/pdf.py:405 msgid "Employee number" msgstr "Medewerkernummer" -#: amelie/members/models.py:925 +#: amelie/members/models.py:936 msgid "employee" msgstr "medewerker" -#: amelie/members/models.py:926 +#: amelie/members/models.py:937 msgid "employees" msgstr "medewerkers" -#: amelie/members/models.py:958 +#: amelie/members/models.py:969 msgid "External photographers should at least have a first- and lastname." msgstr "Een externe fotograaf moet op zijn minst een voor- en achternaam hebben." -#: amelie/members/models.py:983 -#: amelie/personal_tab/templates/balance/balance.html:60 -#: amelie/personal_tab/templates/balance/balance.html:115 +#: amelie/members/models.py:994 +#: amelie/personal_tab/templates/balance/balance.html:69 +#: amelie/personal_tab/templates/balance/balance.html:140 msgid "Member" msgstr "Lid" -#: amelie/members/models.py:986 +#: amelie/members/models.py:997 msgid "Ended preliminary" msgstr "Voortijdig beëindigd" -#: amelie/members/models.py:987 +#: amelie/members/models.py:998 msgid "Verified on" msgstr "Geverifiëerd op" -#: amelie/members/models.py:990 amelie/members/models.py:1365 -#: amelie/personal_tab/models.py:522 +#: amelie/members/models.py:1001 amelie/members/models.py:1443 +#: amelie/personal_tab/models.py:558 msgid "Documenso ID" msgstr "Documenso-ID" -#: amelie/members/models.py:991 amelie/personal_tab/models.py:523 +#: amelie/members/models.py:1002 amelie/personal_tab/models.py:559 msgid "Signed document" msgstr "Getekend document" -#: amelie/members/models.py:995 +#: amelie/members/models.py:1006 msgid "membership" msgstr "lidmaatschap" -#: amelie/members/models.py:996 +#: amelie/members/models.py:1007 msgid "memberships" msgstr "lidmaatschappen" -#: amelie/members/models.py:1054 amelie/members/models.py:1510 -#: amelie/personal_tab/models.py:636 +#: amelie/members/models.py:1100 +#, fuzzy, python-brace-format +#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgid "Payment for contribution {membership_type} ({begin_year}/{end_year})" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/members/models.py:1153 amelie/members/models.py:1589 +#: amelie/personal_tab/models.py:672 msgid "Documenso ID is not set. No documents are due to be signed." msgstr "Documenso-ID is niet ingesteld. Er zijn geen documenten wachtend op ondertekening." -#: amelie/members/models.py:1080 -#: amelie/members/templates/includes/query/query_membership.html:3 -#: amelie/personal_tab/models.py:250 amelie/tools/pdf.py:73 -#: amelie/tools/pdf.py:155 templates/basis.html:210 -#: templates/profile_overview.html:45 -msgid "Membership" -msgstr "Lidmaatschap" - -#: amelie/members/models.py:1084 -msgid "payment" -msgstr "betaling" - -#: amelie/members/models.py:1085 -msgid "payments" -msgstr "betalingen" - -#: amelie/members/models.py:1100 +#: amelie/members/models.py:1178 msgid "committee category" msgstr "commissiecategorie" -#: amelie/members/models.py:1101 +#: amelie/members/models.py:1179 msgid "committee categories" msgstr "commissiecategorieën" -#: amelie/members/models.py:1120 +#: amelie/members/models.py:1198 msgid "You can only enter ^[a-zA-Z0-9.-]*$." msgstr "Je mag alleen ^[a-zA-Z0-9.-]*$ invullen." -#: amelie/members/models.py:1125 +#: amelie/members/models.py:1203 msgid "Parent committees" msgstr "Oudercommissies" -#: amelie/members/models.py:1131 amelie/members/models.py:1290 +#: amelie/members/models.py:1209 amelie/members/models.py:1368 msgid "Started on" msgstr "Begonnen op" -#: amelie/members/models.py:1132 amelie/members/models.py:1291 +#: amelie/members/models.py:1210 amelie/members/models.py:1369 msgid "Ended on" msgstr "Beëindigd op" -#: amelie/members/models.py:1133 +#: amelie/members/models.py:1211 #: amelie/members/templates/includes/committee_data.html:36 msgid "Web site" msgstr "Website" -#: amelie/members/models.py:1135 +#: amelie/members/models.py:1213 msgid "Information (en)" msgstr "Informatie (en)" -#: amelie/members/models.py:1137 +#: amelie/members/models.py:1215 msgid "E-mail address is private" msgstr "E-mailadres is privé" -#: amelie/members/models.py:1139 +#: amelie/members/models.py:1217 msgid "The e-mail address of this committee is not displayed on the website" msgstr "Het e-mailadres van deze commissie wordt niet op de website getoond" -#: amelie/members/models.py:1141 +#: amelie/members/models.py:1219 msgid "Is board" msgstr "Is bestuur" -#: amelie/members/models.py:1143 +#: amelie/members/models.py:1221 msgid "Members of this committee are granted board authorities on this web site" msgstr "Leden van deze commissie krijgen bestuursrecht op de website" -#: amelie/members/models.py:1146 +#: amelie/members/models.py:1224 msgid "Members of this committee get access to GitLab" msgstr "Leden van deze commissie krijgen toegang tot GitLab" -#: amelie/members/models.py:1148 +#: amelie/members/models.py:1226 msgid "Members of this committee get a private Matrix space" msgstr "Leden van deze commissie krijgen toegang tot een eigen Matrix-ruimte" -#: amelie/members/models.py:1156 amelie/personal_tab/models.py:35 -#: amelie/personal_tab/models.py:265 +#: amelie/members/models.py:1234 amelie/personal_tab/models.py:40 +#: amelie/personal_tab/models.py:301 msgid "ledger account" msgstr "grootboekrekeningnummer" -#: amelie/members/models.py:1160 amelie/personal_tab/models.py:1022 +#: amelie/members/models.py:1238 amelie/personal_tab/models.py:1434 msgid "committee" msgstr "commissie" -#: amelie/members/models.py:1161 +#: amelie/members/models.py:1239 #: amelie/members/templates/members/new_member.mail:22 msgid "committees" msgstr "commissies" -#: amelie/members/models.py:1170 +#: amelie/members/models.py:1248 msgid "The email address for a committee may only point to an Inter-Actief server." msgstr "Het e-mailadres van een commissie mag alleen maar verwijzen naar een Inter-Actief server." -#: amelie/members/models.py:1287 +#: amelie/members/models.py:1365 #: amelie/members/templates/includes/person_function_table.html:7 #: amelie/members/templates/person_edit_functions.html:16 #: amelie/members/templates/statistics/overview.html:123 -#: amelie/personal_tab/forms.py:221 amelie/personal_tab/forms.py:405 +#: amelie/personal_tab/forms.py:253 amelie/personal_tab/forms.py:443 #: amelie/statistics/templates/statistics.html:58 amelie/tools/pdf.py:548 msgid "Committee" msgstr "Commissie" -#: amelie/members/models.py:1295 +#: amelie/members/models.py:1373 msgid "position" msgstr "functie" -#: amelie/members/models.py:1296 +#: amelie/members/models.py:1374 msgid "functions" msgstr "functies" -#: amelie/members/models.py:1305 amelie/members/models.py:1309 +#: amelie/members/models.py:1383 amelie/members/models.py:1387 msgid "Person is already a member of this committee." msgstr "Persoon zit al in deze commissie." -#: amelie/members/models.py:1349 +#: amelie/members/models.py:1427 msgid "Authorizations" msgstr "Machtigingen" -#: amelie/members/models.py:1352 +#: amelie/members/models.py:1430 msgid "Chosen membership type" msgstr "Gekozen lidmaatschapstype" -#: amelie/members/models.py:1354 +#: amelie/members/models.py:1432 msgid "Membership start year" msgstr "Startjaar lidmaatschap" -#: amelie/members/models.py:1359 +#: amelie/members/models.py:1437 msgid "Studies" msgstr "Studies" -#: amelie/members/models.py:1360 +#: amelie/members/models.py:1438 msgid "Study start date" msgstr "Begindatum studie" @@ -7794,6 +8465,19 @@ msgstr "Heeft een doegroep" msgid "Has no dogroup" msgstr "Heeft geen doegroep" +#: amelie/members/query_forms.py:83 +#: amelie/members/templates/includes/query/query_mandate.html:4 +#: amelie/personal_tab/templates/authorization/view.html:4 +#: amelie/personal_tab/templates/authorization/view.html:8 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:41 +#: amelie/personal_tab/templates/debt_collection/process_batch.html:49 +#: amelie/personal_tab/templates/debt_collection/view.html:92 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:12 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:11 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:33 +msgid "Mandate" +msgstr "Machtiging" + #: amelie/members/query_forms.py:84 msgid "Mandate number" msgstr "Machtigingnummer" @@ -7826,43 +8510,43 @@ msgstr "Ongeldig IBAN." msgid "Invalid characters in name." msgstr "Ongeldige karakters in naam." -#: amelie/members/query_forms.py:383 +#: amelie/members/query_forms.py:385 msgid "Sender's name" msgstr "Naam afzender" -#: amelie/members/query_forms.py:384 +#: amelie/members/query_forms.py:386 msgid "Sender's e-mail" msgstr "Afzender e-mail" -#: amelie/members/query_forms.py:385 +#: amelie/members/query_forms.py:387 msgid "CC" msgstr "CC" -#: amelie/members/query_forms.py:386 +#: amelie/members/query_forms.py:388 msgid "BCC" msgstr "BCC" -#: amelie/members/query_forms.py:392 +#: amelie/members/query_forms.py:394 msgid "Message (Dutch)" msgstr "Bericht (Nederlands)" -#: amelie/members/query_forms.py:394 +#: amelie/members/query_forms.py:396 msgid "Message (English)" msgstr "Bericht (Engels)" -#: amelie/members/query_forms.py:401 +#: amelie/members/query_forms.py:403 msgid "Invalid subject" msgstr "Ongeldig onderwerp" -#: amelie/members/query_forms.py:409 +#: amelie/members/query_forms.py:411 msgid "Invalid template" msgstr "Ongeldig template" -#: amelie/members/query_forms.py:496 +#: amelie/members/query_forms.py:498 msgid "Include people on the waiting list" msgstr "Ook mensen op de wachtlijst" -#: amelie/members/query_views.py:155 amelie/personal_tab/views.py:2006 +#: amelie/members/query_views.py:155 amelie/personal_tab/views.py:2417 msgid "The mails are now being sent one by one. This happens in a background process and might take a while." msgstr "De mails worden nu een voor een verstuurd. Dit gebeurt in een achtergrondproces en kan even duren." @@ -7994,9 +8678,9 @@ msgstr "" #: amelie/members/templates/person_edit_functions.html:18 #: amelie/members/templates/person_edit_study.html:18 #: amelie/members/templates/person_mandate.html:42 -#: amelie/personal_tab/forms.py:145 +#: amelie/personal_tab/forms.py:177 #: amelie/personal_tab/templates/authorization/list.html:70 -#: amelie/personal_tab/templates/authorization/view.html:45 +#: amelie/personal_tab/templates/authorization/view.html:50 #: amelie/personal_tab/templates/info/account_information.html:16 #: amelie/personal_tab/templates/lists/authorization_select.html:13 #: amelie/statistics/models.py:6 @@ -8009,9 +8693,9 @@ msgstr "Begindatum" #: amelie/members/templates/person_edit_functions.html:19 #: amelie/members/templates/person_edit_study.html:19 #: amelie/members/templates/person_mandate.html:43 -#: amelie/personal_tab/forms.py:146 +#: amelie/personal_tab/forms.py:178 #: amelie/personal_tab/templates/authorization/list.html:71 -#: amelie/personal_tab/templates/authorization/view.html:49 +#: amelie/personal_tab/templates/authorization/view.html:54 #: amelie/personal_tab/templates/info/account_information.html:17 #: amelie/statistics/models.py:7 msgid "Ends on" @@ -8050,7 +8734,7 @@ msgstr "Lid sinds" #: amelie/members/templates/committees.html:11 #: amelie/members/templates/person.html:54 #: amelie/members/templates/statistics/overview.html:90 -#: templates/basis.html:209 templates/profile_overview.html:144 +#: templates/basis.html:209 templates/profile_overview.html:150 msgid "Committees" msgstr "Commissies" @@ -8315,6 +8999,14 @@ msgstr "EN:" msgid "Content:" msgstr "Inhoud:" +#: amelie/members/templates/includes/query/query_membership.html:3 +#: amelie/personal_tab/models.py:286 +#: amelie/personal_tab/templates/info/transaction_information.html:101 +#: amelie/tools/pdf.py:73 amelie/tools/pdf.py:155 templates/basis.html:210 +#: templates/profile_overview.html:45 +msgid "Membership" +msgstr "Lidmaatschap" + #: amelie/members/templates/includes/query/query_name.html:3 msgid "Basic data" msgstr "Basisgegevens" @@ -8401,7 +9093,7 @@ msgstr "Contactgegevens" #: amelie/members/templates/includes/registration/authorizations.html:4 #: amelie/members/templates/person.html:26 #: amelie/members/templates/registration_check.html:73 -#: amelie/personal_tab/forms.py:173 +#: amelie/personal_tab/forms.py:205 #: amelie/personal_tab/templates/authorization/list.html:4 #: amelie/personal_tab/templates/authorization/list.html:58 #: amelie/personal_tab/templates/cookie_corner_overview.html:21 @@ -8454,7 +9146,9 @@ msgid "Your IBAN bank account number. For example \"NL18ABNA0484869868\". msgstr "Je IBAN-bankrekeningnummer. Bijvoorbeeld \"NL18ABNA0484869868\"." #: amelie/members/templates/includes/registration/authorizations.html:71 -msgid "Your BIC-code. Not required for Dutch bank accounts. For example \"ABNANL2A\". Find your BIC by clicking here (opens in new tab)." +#, fuzzy +#| msgid "Your BIC-code. Not required for Dutch bank accounts. For example \"ABNANL2A\". Find your BIC by clicking here (opens in new tab)." +msgid "Your BIC-code. Not required for Dutch bank accounts. For example \"ABNANL2A\". Find your BIC by clicking here (opens in new tab)." msgstr "Je BIC-code. Niet vereist voor Nederlandse rekeningnummers. Bijvoorbeeld \"ABNANL2A\". Vind je BIC door hier te klikken (opent in nieuwe tab)." #: amelie/members/templates/includes/registration/contact_member.html:6 @@ -8548,7 +9242,7 @@ msgstr "Medewerker- en Lidmaatschapsgegevens" #: amelie/members/templates/includes/registration/membership_freshmen.html:25 #: amelie/members/templates/includes/registration/membership_general.html:33 #: amelie/members/templates/registration_check.html:59 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:10 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:11 #: amelie/personal_tab/templates/statistics/statistics.html:285 msgid "Membership type" msgstr "Lidmaatschapstype" @@ -8614,7 +9308,7 @@ msgstr "Studie- en Lidmaatschapsgegevens" #: amelie/members/templates/includes/registration/study_freshmen.html:10 #: amelie/members/templates/includes/registration/study_general.html:10 #: amelie/members/templates/person_study.html:9 -#: templates/profile_overview.html:111 +#: templates/profile_overview.html:117 msgid "Study/studies" msgstr "Studie(s)" @@ -9108,21 +9802,6 @@ msgstr "" "\n" "Het bestuur van " -#: amelie/members/templates/members/payment_confirm_delete.html:5 -#: amelie/members/templates/members/payment_confirm_delete.html:10 -msgid "Remove payment" -msgstr "Betaling verwijderen" - -#: amelie/members/templates/members/payment_confirm_delete.html:14 -#, python-format -msgid "" -"\n" -" Are you sure you want to remove the payment %(object)s?\n" -" " -msgstr "" -"\n" -"Weet je zeker dat je betaling %(object)s wilt verwijderen?" - #: amelie/members/templates/members/preregistration_welcome.mail:4 #, python-format msgid "[Inter-Actief] Welcome to Inter-/Actief/, %(name)s, please sign your membership documents!" @@ -9224,6 +9903,7 @@ msgstr "Je kan zelf zien of je studie al is geverifiëerd door naar je profiel o #: amelie/members/templates/members/verification_reminder.mail:14 #: amelie/members/templates/members/yeartransfer_ended.mail:14 +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:17 msgid "If you have any questions you can contact us by replying to this email." msgstr "Als je nog vragen hebt, kun je natuurlijk altijd reageren op deze mail." @@ -9325,7 +10005,7 @@ msgstr "De opdracht wordt uitgevoerd..." #: amelie/members/templates/person.html:35 #: amelie/members/templates/person_study.html:8 -#: templates/profile_overview.html:101 templates/profile_overview.html:110 +#: templates/profile_overview.html:107 templates/profile_overview.html:116 msgid "Student data" msgstr "Studentgegevens" @@ -9334,7 +10014,7 @@ msgstr "Studentgegevens" msgid "Employee data" msgstr "Medewerkergegevens" -#: amelie/members/templates/person.html:76 templates/profile_overview.html:245 +#: amelie/members/templates/person.html:76 templates/profile_overview.html:251 msgid "User accounts and login providers" msgstr "Gebruikersaccounts en inlogproviders" @@ -9364,28 +10044,27 @@ msgstr "De volgende gebruikersaccount(s) zijn gevonden die aan deze persoon gere #: amelie/members/templates/person_accounts.html:8 #: amelie/members/templates/person_accounts.html:44 -#: amelie/personal_tab/templates/register/register_card_index.html:19 -#: templates/profile_overview.html:256 templates/profile_overview.html:304 +#: templates/profile_overview.html:262 templates/profile_overview.html:310 msgid "Username" msgstr "Gebruikersnaam" #: amelie/members/templates/person_accounts.html:9 -#: templates/profile_overview.html:257 +#: templates/profile_overview.html:263 msgid "Account type" msgstr "Accounttype" #: amelie/members/templates/person_accounts.html:10 -#: templates/profile_overview.html:258 +#: templates/profile_overview.html:264 msgid "Linked social accounts" msgstr "Gekoppelde sociale accounts" #: amelie/members/templates/person_accounts.html:17 -#: templates/profile_overview.html:265 +#: templates/profile_overview.html:271 msgid "Inter-Actief Active Members account" msgstr "Inter-Actief-actieveledenaccount" #: amelie/members/templates/person_accounts.html:20 -#: templates/profile_overview.html:267 +#: templates/profile_overview.html:273 msgid "TOTP devices:" msgstr "TOTP-apparaten:" @@ -9394,27 +10073,27 @@ msgid "No TOTP devices" msgstr "Geen TOTP-apparaten" #: amelie/members/templates/person_accounts.html:33 -#: templates/profile_overview.html:293 +#: templates/profile_overview.html:299 msgid "Integration account" msgstr "Integratie-account" #: amelie/members/templates/person_accounts.html:35 -#: templates/profile_overview.html:295 +#: templates/profile_overview.html:301 msgid "University of Twente account" msgstr "Universiteit Twente account" #: amelie/members/templates/person_accounts.html:43 -#: templates/profile_overview.html:303 +#: templates/profile_overview.html:309 msgid "Provider" msgstr "Dienst" #: amelie/members/templates/person_accounts.html:52 -#: templates/profile_overview.html:318 +#: templates/profile_overview.html:324 msgid "No linked accounts" msgstr "Geen gekoppelde accounts" #: amelie/members/templates/person_accounts.html:59 -#: templates/profile_overview.html:325 +#: templates/profile_overview.html:331 msgid "No user accounts could be retrieved" msgstr "Er konden geen accounts opgehaald worden" @@ -9461,6 +10140,31 @@ msgstr "De persoonsgegevens van %(name)s zijn geanonimiseerd." msgid "Return to this person." msgstr "Terug naar deze persoon." +#: amelie/members/templates/person_confirm_mandate.html:3 +msgid "Confirm BIC for mandate" +msgstr "" + +#: amelie/members/templates/person_confirm_mandate.html:12 +#, python-format +msgid "" +"\n" +" The BIC for this mandate is on the list of known BICs that do not accept SEPA Direct Debits.\n" +" Are you sure you want to add a mandate with the following IBAN: %(iban)s and BIC: %(bic)s?\n" +" " +msgstr "" + +#: amelie/members/templates/person_confirm_mandate.html:17 +msgid "" +"\n" +" Only do this if you are sure that this bank accepts SEPA Direct Debits.\n" +" " +msgstr "" + +#: amelie/members/templates/person_confirm_mandate.html:26 +#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:57 +msgid "Confirm" +msgstr "Bevestigen" + #: amelie/members/templates/person_delete.html:5 msgid "Delete account" msgstr "Verwijder account" @@ -9568,7 +10272,7 @@ msgstr "" #: amelie/members/templates/person_edit_mandate.html:27 #: amelie/members/templates/person_new_mandate.html:8 -#: amelie/personal_tab/forms.py:115 +#: amelie/personal_tab/forms.py:113 msgid "BIC is not required for a Dutch IBAN" msgstr "BIC is niet vereist bij een Nederlands IBAN." @@ -9584,6 +10288,7 @@ msgstr "Nieuwe studie toevoegen" #: amelie/members/templates/person_new_mandate.html:14 #: amelie/members/templates/person_new_membership.html:14 #: amelie/members/templates/registration_check.html:120 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:25 #: amelie/personal_tab/templates/exam_cookie_credit/add.html:22 #: amelie/personal_tab/templates/transactions/transaction_form.html:32 #: amelie/room_duty/templates/room_duty/pool/list.html:12 @@ -9690,9 +10395,9 @@ msgid "Add mandate" msgstr "Voeg machtiging toe" #: amelie/members/templates/person_mandate.html:37 -#: amelie/personal_tab/forms.py:140 +#: amelie/personal_tab/forms.py:172 #: amelie/personal_tab/templates/authorization/list.html:66 -#: amelie/personal_tab/templates/authorization/view.html:134 +#: amelie/personal_tab/templates/authorization/view.html:139 #: amelie/personal_tab/templates/debt_collection/process_batch.html:47 #: amelie/personal_tab/templates/debt_collection/view.html:90 #: amelie/personal_tab/templates/info/account_information.html:12 @@ -9702,7 +10407,7 @@ msgstr "Kenmerk" #: amelie/members/templates/person_mandate.html:38 #: amelie/members/templates/person_membership.html:46 -#: amelie/personal_tab/forms.py:152 +#: amelie/personal_tab/forms.py:184 #: amelie/personal_tab/templates/authorization/list.html:67 #: amelie/personal_tab/templates/authorization/view.html:17 #: amelie/personal_tab/templates/info/account_information.html:13 @@ -9717,8 +10422,8 @@ msgid "IBAN/BIC" msgstr "IBAN/BIC" #: amelie/members/templates/person_mandate.html:40 -#: amelie/personal_tab/forms.py:142 -#: amelie/personal_tab/templates/authorization/view.html:41 +#: amelie/personal_tab/forms.py:174 +#: amelie/personal_tab/templates/authorization/view.html:46 #: amelie/personal_tab/templates/info/account_information.html:15 #: amelie/tools/pdf.py:393 msgid "Account holder" @@ -9729,9 +10434,9 @@ msgid "Signed" msgstr "Getekend" #: amelie/members/templates/person_mandate.html:71 -#: amelie/personal_tab/templates/authorization/list.html:105 +#: amelie/personal_tab/templates/authorization/list.html:110 #: amelie/personal_tab/templates/info/account_information.html:42 -#: amelie/personal_tab/templates/lists/authorization_select.html:46 +#: amelie/personal_tab/templates/lists/authorization_select.html:51 msgid "before September 2013" msgstr "voor september 2013" @@ -9756,10 +10461,10 @@ msgid "View signature" msgstr "Handtekening tonen" #: amelie/members/templates/person_mandate.html:94 -#: amelie/members/templates/person_membership.html:96 +#: amelie/members/templates/person_membership.html:149 #: amelie/members/templates/preregistration_resend_confirm.html:4 #: amelie/members/templates/preregistration_resend_confirm.html:8 -#: amelie/members/templates/preregistration_status.html:76 +#: amelie/members/templates/preregistration_status.html:84 msgid "Re-send digital signature request" msgstr "Digitaal ondertekeningsverzoek opnieuw versturen" @@ -9769,7 +10474,7 @@ msgstr "Handtekening opnieuw aanvragen" #: amelie/members/templates/person_mandate.html:96 #: amelie/members/templates/person_mandate_sign.html:3 -#: amelie/members/templates/person_membership.html:98 +#: amelie/members/templates/person_membership.html:151 #: amelie/members/templates/person_membership_sign.html:3 msgid "Send digital signature request" msgstr "Verstuur digitaal ondertekeningsverzoek" @@ -9787,8 +10492,8 @@ msgid "View in sign tool" msgstr "Tonen in ondertekeningstool" #: amelie/members/templates/person_mandate.html:102 -#: amelie/members/templates/person_membership.html:104 -#: amelie/members/templates/preregistration_status.html:82 +#: amelie/members/templates/person_membership.html:157 +#: amelie/members/templates/preregistration_status.html:90 msgid "Try to retrieve signed file from Documenso" msgstr "Probeer het ondertekende document op te halen uit Documenso" @@ -9818,11 +10523,6 @@ msgstr "Het volgende document is succesvol opgehaald uit Dcouemso en opgeslagen msgid "Could not retrieve any document from Documenso. The error received was:" msgstr "Kon geen documenten ophalen van Documenso. De ontvangen foutmelding is:" -#: amelie/members/templates/person_mandate_retrieve_signed.html:18 -#: amelie/members/templates/person_membership_retrieve_signed.html:18 -msgid "Return" -msgstr "Terug" - #: amelie/members/templates/person_mandate_sign.html:7 msgid "" "\n" @@ -9879,7 +10579,7 @@ msgstr "" "%(obj)s is geen lid." #: amelie/members/templates/person_membership.html:21 -#: amelie/members/templates/person_membership.html:126 +#: amelie/members/templates/person_membership.html:179 msgid "Add membership" msgstr "Voeg lidmaatschap toe" @@ -9911,39 +10611,77 @@ msgstr "%(obj)s is niet lid." msgid "Course year" msgstr "Collegejaar" +#: amelie/members/templates/person_membership.html:47 +msgid "Paid?" +msgstr "" + #: amelie/members/templates/person_membership.html:63 #, python-format msgid "Ended as of %(ended)s" msgstr "Beëindigd per %(ended)s" -#: amelie/members/templates/person_membership.html:75 -msgid "Delete payment" -msgstr "Verwijder betaling" +#: amelie/members/templates/person_membership.html:72 +msgid "N/A" +msgstr "" -#: amelie/members/templates/person_membership.html:84 -msgid "Process payment" -msgstr "Voer betaling in" +#: amelie/members/templates/person_membership.html:75 +msgid "For some reason this free membership has payments! This should not happen." +msgstr "" -#: amelie/members/templates/person_membership.html:90 +#: amelie/members/templates/person_membership.html:143 msgid "Download PDF form" msgstr "Download PDF-formulier" -#: amelie/members/templates/person_membership.html:92 +#: amelie/members/templates/person_membership.html:145 msgid "View signed member form" msgstr "Ondertekend lidmaatschapsformulier tonen" -#: amelie/members/templates/person_membership.html:103 +#: amelie/members/templates/person_membership.html:156 msgid "A signature request for this membership is in progress. Click to open the request in Documenso." msgstr "Een ondertekeningsverzoek voor deze machtiging is in behandeling. Klik om het verzoek te openen in Documenso." -#: amelie/members/templates/person_membership.html:107 +#: amelie/members/templates/person_membership.html:160 msgid "This membership was created before we introduced digital signatures. Its signed copy may be in the physical folders." msgstr "Dit lidmaatschap is aangemaakt voordat we digitale handtekeningen hebben geintroduceerd. Het ondertekende exemplaar bevindt zich mogelijk in de fysieke mappen." -#: amelie/members/templates/person_membership.html:122 +#: amelie/members/templates/person_membership.html:175 msgid "Terminate membership" msgstr "Einde lidmaatschap" +#: amelie/members/templates/person_membership_create_payment.html:5 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Do you want to add %(member)s to %(group)s?\n" +#| " " +msgid "" +"\n" +" Create manual payment for %(membership)s of %(person)s\n" +" " +msgstr "" +"\n" +" Wil je %(member)s toevoegen aan %(group)s?\n" +" " + +#: amelie/members/templates/person_membership_payments.html:20 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Do you want to remove %(member)s from %(group)s?\n" +#| " " +msgid "" +"\n" +" Payments for %(membership)s %(year_1)s/%(year_2)s\n" +" " +msgstr "" +"\n" +" Wil je %(member)s verwijderen uit %(group)s?\n" +" " + +#: amelie/members/templates/person_membership_payments.html:62 +msgid "No transactions found. If this is a paid membership, this is probably wrong! Please contact the WWW." +msgstr "" + #: amelie/members/templates/person_membership_retrieve_signed.html:6 msgid "The following document was successfully retrieved from Documenso and saved to the membership:" msgstr "Het volgende document is succesvol opgehaald uit Dcouemso en opgeslagen in het lidmaatschap:" @@ -9990,14 +10728,6 @@ msgstr "" msgid "Add a membership" msgstr "Lidmaatschap toevoegen" -#: amelie/members/templates/person_payment.html:3 -msgid "Execute payment" -msgstr "Betaling invoeren" - -#: amelie/members/templates/person_payment.html:18 -msgid "Input" -msgstr "Invoeren" - #: amelie/members/templates/person_registration_form_complete.html:5 msgid "Enrollment successful" msgstr "Inschrijving geslaagd" @@ -10060,17 +10790,17 @@ msgid "Pre-enroll new freshman member" msgstr "Nieuw eerstejaarslid voorinschrijven" #: amelie/members/templates/person_study.html:10 -#: templates/profile_overview.html:112 +#: templates/profile_overview.html:118 msgid "Period(s)" msgstr "Periode(s)" #: amelie/members/templates/person_study.html:19 -#: templates/profile_overview.html:121 +#: templates/profile_overview.html:127 msgid "No doegroep registered" msgstr "Geen doegroep geregistreerd" #: amelie/members/templates/person_study.html:21 templates/basis.html:181 -#: templates/profile_overview.html:123 +#: templates/profile_overview.html:129 msgid "Current" msgstr "Actueel" @@ -10114,7 +10844,7 @@ msgid "Print all forms" msgstr "Print alle formulieren" #: amelie/members/templates/preregistration_print_all.html:8 -#: amelie/members/templates/preregistration_status.html:40 +#: amelie/members/templates/preregistration_status.html:48 msgid "Print all membership and authorization forms" msgstr "Print alle lidmaatschaps- en machtigingsformulieren" @@ -10140,8 +10870,6 @@ msgstr "Print die formulieren!" #: amelie/members/templates/preregistration_print_all.html:45 #: amelie/personal_tab/templates/pos/shop.html:13 -#: amelie/personal_tab/templates/register/register_card_error.html:14 -#: amelie/personal_tab/templates/register/register_card_scan.html:54 msgid "Go back" msgstr "Ga terug" @@ -10234,61 +10962,79 @@ msgstr "Dit zijn de huidige voorinschrinvingen die nog niet zijn geactiveerd, pe msgid "If the member has signed their membership and authorization forms, the membership and any authorizations should automatically get activated and it will disappear from this list." msgstr "Als het lid hun lidmaatschaps- en machtigingsformulieren heeft ondertekend, dan zal het lidmaatschap en de eventuele machtigingen automatisch worden geactiveerd, en zal het lid verdwijnen uit deze lijst." -#: amelie/members/templates/preregistration_status.html:27 +#: amelie/members/templates/preregistration_status.html:30 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " With this form, you can edit the amendment to the %(authorization)s authorization.\n" +#| " " +msgid "" +"\n" +" To view already activated memberships from this academic year, check the current enrollment status instead.\n" +" " +msgstr "" +"\n" +" Met dit formulier kun je een amendement aanpassen voor machtiging %(authorization)s.\n" +" " + +#: amelie/members/templates/preregistration_status.html:35 msgid "You can (re-)print the forms for that member by clicking the \"Print\" () button." msgstr "Je kunt de formulieren van een lid (opnieuw) printen door op de \"Print\" () knop te drukken." -#: amelie/members/templates/preregistration_status.html:29 +#: amelie/members/templates/preregistration_status.html:37 msgid "Additionally, as a board member, you can view the state of the signature requests (), and attempt re-download of the signed forms () or even re-send them () if it failed for whatever reason." msgstr "Daarnaast kun je als bestuurslid de status van ondertekeningsverzoeken zien (), en proberen de ondertekende documenten opnieuw op te halen () of deze zelfs opnieuw te versturen naar het lid () als dit voor een of andere reden is mislukt." -#: amelie/members/templates/preregistration_status.html:32 +#: amelie/members/templates/preregistration_status.html:40 msgid "Click the \"Delete\" () button to delete the pre-enrollment." msgstr "Klik op de \"Verwijder\" () knop om de voorinschrijving te verwijderen." -#: amelie/members/templates/preregistration_status.html:37 +#: amelie/members/templates/preregistration_status.html:45 msgid "Pre-enroll member" msgstr "Lid voorinschrijven" -#: amelie/members/templates/preregistration_status.html:45 +#: amelie/members/templates/preregistration_status.html:53 msgid "Disable deletions" msgstr "Verwijderknoppen uitschakelen" -#: amelie/members/templates/preregistration_status.html:51 +#: amelie/members/templates/preregistration_status.html:59 msgid "Enable deletions" msgstr "Verwijder-knoppen activeren" -#: amelie/members/templates/preregistration_status.html:65 +#: amelie/members/templates/preregistration_status.html:73 #, python-format msgid "%(len)s enrollment(s)" msgstr "%(len)s inschrijving(en)" -#: amelie/members/templates/preregistration_status.html:73 +#: amelie/members/templates/preregistration_status.html:81 msgid "Print membership forms (opens in new tab)" msgstr "Lidmaatschapsformulieren printen (opent in nieuwe tab)" -#: amelie/members/templates/preregistration_status.html:80 +#: amelie/members/templates/preregistration_status.html:88 msgid "A signature request for this membership is in progress. Click to open the request in Documenso" msgstr "Een ondertekeningsverzoek voor dit lidmaatschap is in behandeling. Klik om het verzoek te openen in Documenso." -#: amelie/members/templates/preregistration_status.html:87 +#: amelie/members/templates/preregistration_status.html:95 msgid "Manually activate. Should not be necessary and will not include any digital signature files" msgstr "Handmatig activeren. Zou niet nodig moeten zijn en er zullen geen digitaal getekende bestanden worden toegevoegd" -#: amelie/members/templates/preregistration_status.html:90 +#: amelie/members/templates/preregistration_status.html:98 msgid "Delete this pre-enrollment" msgstr "Verwijder deze voorinschrijving" -#: amelie/members/templates/preregistration_status.html:94 +#: amelie/members/templates/preregistration_status.html:102 +#: amelie/members/templates/registration_status.html:41 msgid "No members" msgstr "Geen leden" -#: amelie/members/templates/preregistration_status.html:103 +#: amelie/members/templates/preregistration_status.html:111 msgid "No pre-enrollments" msgstr "Geen voorinschrijvingen" -#: amelie/members/templates/preregistration_status.html:106 -msgid "There are currently no pre-enrollments." +#: amelie/members/templates/preregistration_status.html:114 +#, fuzzy +#| msgid "There are currently no pre-enrollments." +msgid "There are currently no pending pre-enrollments." msgstr "Er zijn momenteel geen voorinschrijvingen." #: amelie/members/templates/query.html:5 amelie/members/templates/query.html:63 @@ -10347,7 +11093,7 @@ msgid "Card" msgstr "Kaart" #: amelie/members/templates/registration_check.html:108 -#: amelie/personal_tab/models.py:382 +#: amelie/personal_tab/models.py:418 #: amelie/personal_tab/templates/info/rfid_cards.html:31 msgid "Activated" msgstr "Geactiveerd" @@ -10362,6 +11108,50 @@ msgstr "Niet geactiveerd" msgid "You didn't register any rfid cards." msgstr "Er zijn geen rfid-kaarten geregistreerd." +#: amelie/members/templates/registration_status.html:5 +#: amelie/members/templates/registration_status.html:11 +#, fuzzy +#| msgid "enrollment start" +msgid "Enrollment status" +msgstr "inschrijving start" + +#: amelie/members/templates/registration_status.html:14 +msgid "These are the new members this association year, per do-group. These memberships are already activated." +msgstr "" + +#: amelie/members/templates/registration_status.html:18 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " For this activity, there are %(places)s places left.\n" +#| " " +msgid "" +"\n" +" To view unactivated memberships, check the current pre-enrollment status instead.\n" +" " +msgstr "" +"\n" +" Voor deze activiteit zijn er nog %(places)s plaatsen beschikbaar.\n" +" " + +#: amelie/members/templates/registration_status.html:31 +#, fuzzy, python-format +#| msgid "%(len)s enrollment(s)" +msgid "%(len)s member(s)" +msgstr "%(len)s inschrijving(en)" + +#: amelie/members/templates/registration_status.html:50 +#, fuzzy +#| msgid "No pre-enrollments" +msgid "No enrollments" +msgstr "Geen voorinschrijvingen" + +#: amelie/members/templates/registration_status.html:53 +#, fuzzy +#| msgid "There is no enrollment start date entered." +msgid "There have not been any enrollments this year yet." +msgstr "Er is geen inschrijfstartdatum opgegeven." + #: amelie/members/templates/statistics/overview.html:5 msgid "Member statistics" msgstr "Leden statistieken" @@ -10457,46 +11247,32 @@ msgstr "BIT" msgid "Members per committee" msgstr "Leden per Commissie" -#: amelie/members/templates/statistics/payments.html:5 -msgid "Payment statistics" -msgstr "Betalingen statistieken" - -#: amelie/members/templates/statistics/payments.html:11 -msgid "Payments" -msgstr "Betalingen" - -#: amelie/members/templates/statistics/payments.html:29 -#: amelie/personal_tab/statistics.py:291 -#: amelie/personal_tab/templates/wrapped/wrapped.html:129 -msgid "Totals" -msgstr "Totalen" - -#: amelie/members/views.py:254 +#: amelie/members/views.py:253 msgid "6 or more" msgstr "6 of meer" -#: amelie/members/views.py:271 +#: amelie/members/views.py:270 msgid "total" msgstr "Totaal" -#: amelie/members/views.py:348 +#: amelie/members/views.py:327 msgid "This person still has an active (unterminated) membership." msgstr "Deze persoon heeft nog een actief (niet beeindigd) lidmaatschap." -#: amelie/members/views.py:352 +#: amelie/members/views.py:331 #, python-brace-format msgid "This person has not paid one of their memberships ({year}/{next_year} {type})." msgstr "Deze persoon heeft een lidmaatschap nog niet betaald ({year}/{next_year} {type})." -#: amelie/members/views.py:361 +#: amelie/members/views.py:337 msgid "This person still has an outstanding balance on their personal tab." msgstr "Deze persoon heeft nog een openstaand streeplijstsaldo." -#: amelie/members/views.py:364 +#: amelie/members/views.py:340 msgid "This person isn't enrolled for one or more future activities." msgstr "Deze persoon staat ingeschreven voor een of meer toekomstige activiteit(en)." -#: amelie/members/views.py:379 +#: amelie/members/views.py:355 msgid "" "This person cannot be anonymized because of the following reasons:\n" "{}" @@ -10509,19 +11285,6 @@ msgstr "" msgid "OAuth link code and instructions were sent to {name}." msgstr "OAuth link code en instructies zijn verzonden naar {name}." -#: amelie/members/views.py:1675 -msgid "You are not allowed to manually delete the payments of free memberships." -msgstr "Je mag van gratis lidmaatschappen de betaling niet handmatig verwijderen." - -#: amelie/members/views.py:1678 -msgid "You are not allowed to delete payments that are already (being) debited." -msgstr "Je mag geen betalingen verwijderen die al geïncasseerd zijn/worden." - -#: amelie/members/views.py:1682 -#, python-format -msgid "Payment %(object)s has been removed" -msgstr "Betaling %(object)s is verwijderd" - #: amelie/narrowcasting/templates/index.html:8 msgid "Narrow Casting" msgstr "Narrow Casting" @@ -10840,747 +11603,1006 @@ msgstr "Je hebt de volgende informatie ingevoerd" msgid "Application Name" msgstr "Applicatienaam" -#: amelie/personal_tab/debt_collection.py:99 +#: amelie/personal_tab/debt_collection.py:92 #, python-brace-format msgid "Contribution Inter-Actief {membership_year} {membership_type} {name} Questions? Email treasurer@inter-actief.net" msgstr "Contributie Inter-Actief {membership_year} {membership_type} {name} Voor vragen bel 053-489 3756" -#: amelie/personal_tab/debt_collection.py:158 +#: amelie/personal_tab/debt_collection.py:149 #, python-brace-format msgid "Direct withdrawal of contribution {date}" msgstr "Incasso contributie {date}" -#: amelie/personal_tab/debt_collection.py:159 -#, python-brace-format -msgid "Contribution {membership_type} ({begin_year}/{end_year})" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" - -#: amelie/personal_tab/debt_collection.py:222 +#: amelie/personal_tab/debt_collection.py:205 #, python-brace-format msgid "Personal tab Inter-Actief till {date} {name} Questions? Email treasurer@inter-actief.net" msgstr "Streeplijst Inter-Actief tot {date} {name} Vragen? E-mail treasurer@inter-actief.net" -#: amelie/personal_tab/debt_collection.py:287 +#: amelie/personal_tab/debt_collection.py:269 #, python-brace-format msgid "Direct withdrawal personal tab {date}" msgstr "Incasso streeplijst {date}" -#: amelie/personal_tab/debt_collection.py:315 +#: amelie/personal_tab/debt_collection.py:297 #, python-brace-format msgid "Reversal of direct withdrawal {date}" msgstr "Terugboeking incasso {date}" -#: amelie/personal_tab/debt_collection.py:327 +#: amelie/personal_tab/debt_collection.py:306 #, python-brace-format msgid "Reversal contribution {membership_type} ({start_year}/{end_year})" msgstr "Terugboeking contributie {membership_type} ({start_year}/{end_year})" -#: amelie/personal_tab/forms.py:56 +#: amelie/personal_tab/forms.py:55 msgid "Description for within Inter-Actief" msgstr "Omschrijving voor Inter-Actief" -#: amelie/personal_tab/forms.py:57 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:62 +#: amelie/personal_tab/forms.py:56 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:67 #: amelie/personal_tab/templates/debt_collection/process_batch.html:20 #: amelie/personal_tab/templates/debt_collection/view.html:60 msgid "Date of execution" msgstr "Uitvoerdatum" -#: amelie/personal_tab/forms.py:58 +#: amelie/personal_tab/forms.py:57 #: amelie/personal_tab/templates/balance/balance.html:29 #: amelie/personal_tab/templates/cookie_corner_transactions.html:39 -#: amelie/personal_tab/templates/lists/transactions_totals.html:21 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:61 +#: amelie/personal_tab/templates/lists/transactions_totals.html:22 #: amelie/statistics/templates/statistics.html:79 #: amelie/statistics/templates/statistics_year.html:78 templates/basis.html:305 msgid "Personal tab" msgstr "Streeplijst" -#: amelie/personal_tab/forms.py:59 +#: amelie/personal_tab/forms.py:58 msgid "Transactions until" msgstr "Transacties tot" -#: amelie/personal_tab/forms.py:60 +#: amelie/personal_tab/forms.py:59 #: amelie/personal_tab/templates/statistics/statistics.html:279 msgid "Membership fee" msgstr "Contributie" -#: amelie/personal_tab/forms.py:61 +#: amelie/personal_tab/forms.py:60 msgid "Membership fee years" msgstr "Contributiejaren" -#: amelie/personal_tab/forms.py:77 +#: amelie/personal_tab/forms.py:75 msgid "Date of execution is too soon" msgstr "Uitvoerdatum is te vroeg" -#: amelie/personal_tab/forms.py:79 +#: amelie/personal_tab/forms.py:77 msgid "Date of execution cannot be during the weekend" msgstr "Uitvoerdatum kan niet in het weekend vallen" -#: amelie/personal_tab/forms.py:87 +#: amelie/personal_tab/forms.py:85 msgid "Please select at least one year for the contribution debt collection." msgstr "Selecteer ten minste 1 jaar voor de incassopoging voor contributie." -#: amelie/personal_tab/forms.py:119 amelie/personal_tab/models.py:687 +#: amelie/personal_tab/forms.py:117 amelie/personal_tab/models.py:723 msgid "Short description of the reason for the amendment" msgstr "Korte omschrijving reden van amendement" +#: amelie/personal_tab/forms.py:131 +#, fuzzy +#| msgid "Payment" +msgid "Payment date" +msgstr "Betaling" + #: amelie/personal_tab/forms.py:134 +#, fuzzy +#| msgid "amount" +msgid "Paid amount" +msgstr "bedrag" + +#: amelie/personal_tab/forms.py:135 +msgid "The amount that was paid in this settlement. Can be different from the amount that needs to be paid, can even be negative (if money was paid out by the association). Useful if, say, the transactions add up to 9 euro, you can enter a payment of 10 euro, which leaves 1 euro extra as balance on the person's personal tab." +msgstr "" + +#: amelie/personal_tab/forms.py:166 msgid "Not signed" msgstr "Niet getekend" -#: amelie/personal_tab/forms.py:136 +#: amelie/personal_tab/forms.py:168 msgid "Ended" msgstr "Beëindigd" -#: amelie/personal_tab/forms.py:154 +#: amelie/personal_tab/forms.py:186 msgid "Sort by" msgstr "Sorteer op" -#: amelie/personal_tab/forms.py:155 +#: amelie/personal_tab/forms.py:187 msgid "Sort in reverse" msgstr "Sorteer omgekeerd" -#: amelie/personal_tab/forms.py:184 +#: amelie/personal_tab/forms.py:216 msgid "Tables:" msgstr "Tabellen:" -#: amelie/personal_tab/forms.py:192 +#: amelie/personal_tab/forms.py:224 msgid "Find person" msgstr "Persoon zoeken" -#: amelie/personal_tab/forms.py:202 +#: amelie/personal_tab/forms.py:234 msgid "-- No printers available --" msgstr "-- Geen printers beschikbaar --" -#: amelie/personal_tab/forms.py:208 +#: amelie/personal_tab/forms.py:240 msgid "PDF Document" msgstr "PDF-document" -#: amelie/personal_tab/forms.py:209 +#: amelie/personal_tab/forms.py:241 msgid "Select a PDF document to print" msgstr "Selecteer een PDF-document om te printen" -#: amelie/personal_tab/forms.py:213 +#: amelie/personal_tab/forms.py:245 msgid "Printer" msgstr "Printer" -#: amelie/personal_tab/forms.py:215 +#: amelie/personal_tab/forms.py:247 msgid "Select the printer to use for printing" msgstr "Selecteer de printer die je wilt gebruiken om te printen" -#: amelie/personal_tab/forms.py:219 +#: amelie/personal_tab/forms.py:251 msgid "-- Pay via personal tab --" msgstr "-- Betalen via streeplijst --" -#: amelie/personal_tab/forms.py:222 +#: amelie/personal_tab/forms.py:254 msgid "Select a committee for free printing, or leave blank to pay for printing" msgstr "Selecteer een commissie om gratis te printen, of laat leeg om te betalen voor het printen" -#: amelie/personal_tab/forms.py:226 +#: amelie/personal_tab/forms.py:258 msgid "Copies" msgstr "Kopieën" -#: amelie/personal_tab/forms.py:227 +#: amelie/personal_tab/forms.py:259 msgid "Number of copies to print" msgstr "Aantal kopieën om te printen" -#: amelie/personal_tab/forms.py:233 +#: amelie/personal_tab/forms.py:265 msgid "Dual-sided printing" msgstr "Dubbelzijdig printen" -#: amelie/personal_tab/forms.py:234 +#: amelie/personal_tab/forms.py:266 msgid "Print both sides of the page. Leave unchecked for single-sided printing." msgstr "Print aan beide zijden van de pagina. Laat uitgevinkt voor enkelzijdig printen." -#: amelie/personal_tab/forms.py:247 +#: amelie/personal_tab/forms.py:279 #: amelie/personal_tab/templates/print/form.html:173 msgid "Please select a PDF file." msgstr "Selecteer a.u.b. een PDF-bestand." -#: amelie/personal_tab/forms.py:251 +#: amelie/personal_tab/forms.py:283 msgid "Only PDF files are allowed." msgstr "Alleen PDF-bestanden zijn toegestaan." -#: amelie/personal_tab/forms.py:256 +#: amelie/personal_tab/forms.py:288 msgid "File size cannot exceed 50MB." msgstr "Bestand mag maximaal 50MB groot zijn." -#: amelie/personal_tab/forms.py:267 +#: amelie/personal_tab/forms.py:299 msgid "You are not a member of the committee you want to print for." msgstr "Je bent geen lid van de commissie waarvoor je probeert te printen." -#: amelie/personal_tab/forms.py:271 +#: amelie/personal_tab/forms.py:303 msgid "You do not have an active consumption mandate or are not a member anymore, so we cannot add the costs for this print to your personal tab. Contact the board for help." msgstr "Je hebt geen actieve streeplijstmachtiging of je bent geen lid meer, dus we kunnen de kosten van deze printopdracht niet van je streeplijst afschrijven. Neem contact op met het bestuur voor hulp." -#: amelie/personal_tab/forms.py:331 +#: amelie/personal_tab/forms.py:363 msgid "Printing article not found. Please contact the WWW committee." msgstr "Printartikel niet gevonden. Neem a.u.b. contact op met de WWW-commissie." -#: amelie/personal_tab/forms.py:382 +#: amelie/personal_tab/forms.py:414 msgid "My committees" msgstr "Mijn commissies" -#: amelie/personal_tab/forms.py:386 +#: amelie/personal_tab/forms.py:418 msgid "Other committees" msgstr "Andere commissies" -#: amelie/personal_tab/forms.py:403 +#: amelie/personal_tab/forms.py:441 msgid "-- No Committee --" msgstr "-- Geen commissie --" -#: amelie/personal_tab/forms.py:406 +#: amelie/personal_tab/forms.py:444 msgid "Select a committee for which this declaration is, or leave blank and specify in the description." msgstr "Selecteer een commissie waar deze declaratie voor is, of laat het leeg en leg uit waar het voor is in de omschrijving." -#: amelie/personal_tab/forms.py:411 amelie/tools/pdf.py:568 +#: amelie/personal_tab/forms.py:449 amelie/tools/pdf.py:568 msgid "Payment Method" msgstr "Betalingsmethode" -#: amelie/personal_tab/forms.py:412 +#: amelie/personal_tab/forms.py:450 msgid "Select how you paid for the expense." msgstr "Selecteer hoe je hebt betaald voor de uitgave." -#: amelie/personal_tab/forms.py:418 +#: amelie/personal_tab/forms.py:456 msgid "Select your IBAN for payment." msgstr "Selecteer je IBAN voor betaling." -#: amelie/personal_tab/forms.py:423 +#: amelie/personal_tab/forms.py:461 msgid "Fill in your IBAN if it is not listed above." msgstr "Vul je IBAN in als deze niet hierboven staat." -#: amelie/personal_tab/forms.py:429 +#: amelie/personal_tab/forms.py:467 msgid "Enter the amount of your declaration in euro's." msgstr "Vul het bedrag in van je declaratie, in euros." -#: amelie/personal_tab/forms.py:437 +#: amelie/personal_tab/forms.py:475 msgid "Enter the description for your declaration." msgstr "Vul een omschrijving in voor je declaratie." -#: amelie/personal_tab/forms.py:443 +#: amelie/personal_tab/forms.py:481 msgid "Select documents to attach to the declaration." msgstr "Selecteer documenten om toe te voegen aan de declaratie." -#: amelie/personal_tab/forms.py:459 +#: amelie/personal_tab/forms.py:485 +#, fuzzy +#| msgid "Is Inter-Actief" +msgid "Inter-Actief" +msgstr "Is Inter-Actief" + +#: amelie/personal_tab/forms.py:487 +msgid "Bookkeeping" +msgstr "" + +#: amelie/personal_tab/forms.py:488 +msgid "" +"Select the bookkeeping where the declaration should be sent.\n" +"If you do not know what this means, do not change it." +msgstr "" + +#: amelie/personal_tab/forms.py:505 msgid "-- Other --" msgstr "-- Anders --" -#: amelie/personal_tab/forms.py:469 +#: amelie/personal_tab/forms.py:514 #, python-brace-format msgid "You can upload a maximum of {max_files} files." msgstr "Je kunt maximaal {max_files} bestanden uploaden." -#: amelie/personal_tab/forms.py:475 +#: amelie/personal_tab/forms.py:520 #, python-brace-format msgid "The total file size cannot exceed {max_size} MB." msgstr "De totale bestandsgrootte mag maximaal %(max_size)s MB zijn." -#: amelie/personal_tab/forms.py:491 +#: amelie/personal_tab/forms.py:536 msgid "Please select either an IBAN from the list or enter a custom IBAN, not both." msgstr "Selecteer ofwel een IBAN uit de lijst, of vul er zelf een in, niet beide." -#: amelie/personal_tab/forms.py:497 +#: amelie/personal_tab/forms.py:542 msgid "Please select an IBAN from the list or enter a custom IBAN." msgstr "Selecteer een IBAN uit de lijst, of vul zelf een IBAN in." -#: amelie/personal_tab/forms.py:502 +#: amelie/personal_tab/forms.py:547 msgid "The amount cannot be zero." msgstr "Het bedrag kan niet nul zijn." -#: amelie/personal_tab/forms.py:507 +#: amelie/personal_tab/forms.py:552 msgid "The amount must be a positive number." msgstr "Het bedrag moet positief zijn." -#: amelie/personal_tab/forms.py:512 +#: amelie/personal_tab/forms.py:557 msgid "Please provide a description for your declaration." msgstr "Vul een omschrijving in voor je declaratie." -#: amelie/personal_tab/models.py:29 amelie/personal_tab/models.py:716 +#: amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py:175 +#, python-brace-format +msgid "Settlement for transactions handled before SEPA debits were introduced - {person_name}" +msgstr "" + +#: amelie/personal_tab/models.py:34 amelie/personal_tab/models.py:752 msgid "begin" msgstr "begin" -#: amelie/personal_tab/models.py:30 amelie/personal_tab/models.py:719 +#: amelie/personal_tab/models.py:35 amelie/personal_tab/models.py:755 msgid "end" msgstr "eind" -#: amelie/personal_tab/models.py:31 amelie/personal_tab/models.py:94 -#: amelie/personal_tab/models.py:710 amelie/personal_tab/models.py:833 +#: amelie/personal_tab/models.py:36 amelie/personal_tab/models.py:99 +#: amelie/personal_tab/models.py:746 amelie/personal_tab/models.py:854 #: amelie/publications/models.py:19 msgid "description" msgstr "omschrijving" -#: amelie/personal_tab/models.py:32 +#: amelie/personal_tab/models.py:37 msgid "description (en)" msgstr "omschrijving (en)" -#: amelie/personal_tab/models.py:34 +#: amelie/personal_tab/models.py:39 msgid "articles" msgstr "artikelen" -#: amelie/personal_tab/models.py:36 +#: amelie/personal_tab/models.py:41 msgid "balance account number" msgstr "balansrekeningnummer" -#: amelie/personal_tab/models.py:52 amelie/personal_tab/models.py:60 -#: amelie/personal_tab/models.py:90 +#: amelie/personal_tab/models.py:57 amelie/personal_tab/models.py:65 +#: amelie/personal_tab/models.py:95 msgid "discount offer" msgstr "kortingsactie" -#: amelie/personal_tab/models.py:53 +#: amelie/personal_tab/models.py:58 msgid "discount offers" msgstr "kortingsacties" -#: amelie/personal_tab/models.py:57 amelie/personal_tab/models.py:92 -#: amelie/personal_tab/models.py:836 +#: amelie/personal_tab/models.py:62 amelie/personal_tab/models.py:97 +#: amelie/personal_tab/models.py:857 msgid "amount" msgstr "bedrag" -#: amelie/personal_tab/models.py:58 amelie/personal_tab/models.py:91 -#: amelie/personal_tab/models.py:674 +#: amelie/personal_tab/models.py:63 amelie/personal_tab/models.py:96 +#: amelie/personal_tab/models.py:710 msgid "date" msgstr "datum" -#: amelie/personal_tab/models.py:67 amelie/personal_tab/models.py:95 -#: amelie/personal_tab/models.py:135 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:13 +#: amelie/personal_tab/models.py:72 amelie/personal_tab/models.py:100 +#: amelie/personal_tab/models.py:140 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:14 #: amelie/personal_tab/templates/pos/success.html:37 #: amelie/personal_tab/templates/statistics/statistics.html:58 #: amelie/personal_tab/templates/statistics/statistics.html:104 -#: amelie/personal_tab/templates/statistics/statistics.html:145 msgid "discount" msgstr "korting" -#: amelie/personal_tab/models.py:68 +#: amelie/personal_tab/models.py:73 msgid "discounts" msgstr "kortingen" -#: amelie/personal_tab/models.py:97 +#: amelie/personal_tab/models.py:102 msgid "added on" msgstr "toegevoegd op" -#: amelie/personal_tab/models.py:100 +#: amelie/personal_tab/models.py:105 msgid "added by" msgstr "toegevoegd door" -#: amelie/personal_tab/models.py:104 +#: amelie/personal_tab/models.py:109 msgid "Discount balance" msgstr "Kortingstegoeden" -#: amelie/personal_tab/models.py:105 +#: amelie/personal_tab/models.py:110 msgid "discount balances" msgstr "kortingstegoeden" -#: amelie/personal_tab/models.py:137 -#: amelie/personal_tab/templates/info/transaction_information.html:15 -msgid "Direct withdrawal" -msgstr "Incasso" +#: amelie/personal_tab/models.py:142 +#, fuzzy +#| msgid "pre-settlement" +msgid "Settlement" +msgstr "pre-settlement" -#: amelie/personal_tab/models.py:147 +#: amelie/personal_tab/models.py:152 #: amelie/personal_tab/templates/print/refund.html:48 msgid "Transaction" msgstr "Transactie" -#: amelie/personal_tab/models.py:148 -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:33 +#: amelie/personal_tab/models.py:153 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:41 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:93 #: amelie/personal_tab/templates/cookie_corner_transactions.html:4 #: amelie/personal_tab/templates/cookie_corner_transactions.html:9 #: amelie/personal_tab/templates/cookie_corner_transactions.html:28 #: amelie/personal_tab/templates/cookie_corner_transactions_form.html:4 #: amelie/personal_tab/templates/cookie_corner_transactions_form.html:9 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:172 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:177 #: amelie/personal_tab/templates/statistics/statistics.html:187 #: amelie/personal_tab/templates/transactions/activity_transactions.html:4 #: amelie/personal_tab/templates/transactions/activity_transactions.html:9 msgid "Transactions" msgstr "Transacties" -#: amelie/personal_tab/models.py:192 -#: amelie/personal_tab/templates/lists/activity_transactions.html:13 +#: amelie/personal_tab/models.py:164 amelie/personal_tab/views.py:1008 +#, fuzzy +#| msgid "No activity transactions." +msgid "Activity Transaction" +msgstr "Geen activiteittransacties." + +#: amelie/personal_tab/models.py:166 amelie/personal_tab/views.py:1009 +#, fuzzy +#| msgid "Alexia transactions" +msgid "Alexia Transaction" +msgstr "Alexiatransacties" + +#: amelie/personal_tab/models.py:168 amelie/personal_tab/views.py:1010 +#, fuzzy +#| msgid "Contribution transactions" +msgid "Contribution Transaction" +msgstr "Contributietransacties" + +#: amelie/personal_tab/models.py:170 amelie/personal_tab/views.py:1007 +#, fuzzy +#| msgid "Cookie Corner Login" +msgid "Cookie Corner Transaction" +msgstr "Cookie Corner Login" + +#: amelie/personal_tab/models.py:172 amelie/personal_tab/views.py:1011 +#, fuzzy +#| msgid "Custom transactions" +msgid "Custom Transaction" +msgstr "Overige transacties" + +#: amelie/personal_tab/models.py:174 amelie/personal_tab/views.py:1015 +#, fuzzy +#| msgid "Delete transaction" +msgid "Debt Collection Transaction" +msgstr "Verwijder transactie" + +#: amelie/personal_tab/models.py:176 amelie/personal_tab/views.py:1016 +#, fuzzy +#| msgid "Transaction" +msgid "Reversal Transaction" +msgstr "Transactie" + +#: amelie/personal_tab/models.py:178 amelie/personal_tab/views.py:1018 +#, fuzzy +#| msgid "Balance transaction" +msgid "Extra Balance Transaction" +msgstr "Tegoedtransacties" + +#: amelie/personal_tab/models.py:180 amelie/personal_tab/views.py:1017 +#, fuzzy +#| msgid "Balance transaction" +msgid "Manual Payment Transaction" +msgstr "Tegoedtransacties" + +#: amelie/personal_tab/models.py:228 +#: amelie/personal_tab/templates/lists/activity_transactions.html:14 msgid "More sign up options" msgstr "Met inschrijfopties" -#: amelie/personal_tab/models.py:209 amelie/personal_tab/models.py:311 +#: amelie/personal_tab/models.py:245 amelie/personal_tab/models.py:347 #: amelie/personal_tab/templates/cookie_corner_price_list.html:21 #: amelie/personal_tab/templates/cookie_corner_price_list.html:64 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:11 +#: amelie/personal_tab/templates/info/transaction_information.html:88 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:12 #: amelie/personal_tab/templates/statistics/statistics.html:55 #: amelie/personal_tab/templates/statistics/statistics.html:143 -#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:14 msgid "Article" msgstr "Artikel" -#: amelie/personal_tab/models.py:234 +#: amelie/personal_tab/models.py:270 msgid "Transaction id" msgstr "Transactie ID" -#: amelie/personal_tab/models.py:271 amelie/personal_tab/models.py:294 +#: amelie/personal_tab/models.py:307 amelie/personal_tab/models.py:330 #: amelie/personal_tab/templates/statistics/statistics.html:56 #: amelie/personal_tab/templates/statistics/statistics.html:102 msgid "General ledger account" msgstr "Grootboekrekening" -#: amelie/personal_tab/models.py:272 +#: amelie/personal_tab/models.py:308 msgid "General ledger accounts" msgstr "Grootboekrekeningen" -#: amelie/personal_tab/models.py:297 amelie/personal_tab/models.py:331 +#: amelie/personal_tab/models.py:333 amelie/personal_tab/models.py:367 msgid "Image" msgstr "Afbeelding" -#: amelie/personal_tab/models.py:298 +#: amelie/personal_tab/models.py:334 #: amelie/personal_tab/templates/cookie_corner_price_list.html:23 #: amelie/personal_tab/templates/cookie_corner_price_list.html:66 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:15 -#: amelie/personal_tab/templates/lists/transactions.html:13 -#: amelie/personal_tab/templates/lists/transactions_totals.html:27 +#: amelie/personal_tab/templates/info/transaction_information.html:96 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:16 +#: amelie/personal_tab/templates/lists/transactions.html:14 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:14 +#: amelie/personal_tab/templates/lists/transactions_totals.html:31 #: amelie/personal_tab/templates/pos/success.html:40 -#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:22 msgid "kCal" msgstr "kCal" -#: amelie/personal_tab/models.py:312 +#: amelie/personal_tab/models.py:348 msgid "Articles" msgstr "Artikelen" -#: amelie/personal_tab/models.py:333 +#: amelie/personal_tab/models.py:369 msgid "Show the calculator in the Point of Sale instead of the products" msgstr "Laat de calculator zien in de Point of Sale in plaats van de producten" -#: amelie/personal_tab/models.py:334 +#: amelie/personal_tab/models.py:370 msgid "The first active article in the category will be used for calculations." msgstr "Het eerste actieve artikel in de categorie wordt gebruikt voor berekeningen." -#: amelie/personal_tab/models.py:367 +#: amelie/personal_tab/models.py:403 msgid "Mifare Light" msgstr "Mifare Light" -#: amelie/personal_tab/models.py:368 +#: amelie/personal_tab/models.py:404 msgid "Mifare 1K (Student card/Building card)" msgstr "Mifare 1K (Collegekaart/Gebouwpas)" -#: amelie/personal_tab/models.py:369 +#: amelie/personal_tab/models.py:405 msgid "Mifare 4K (Public transit card)" msgstr "Mifare 4K (OV Chipkaart)" -#: amelie/personal_tab/models.py:370 +#: amelie/personal_tab/models.py:406 msgid "Mifare DESFire" msgstr "Mifare DESFire" -#: amelie/personal_tab/models.py:371 +#: amelie/personal_tab/models.py:407 msgid "Mifare Ultralight (Student card/IA keychain)" msgstr "Mifare Ultralight (Collegekaart/IA-sleutelhanger)" -#: amelie/personal_tab/models.py:372 +#: amelie/personal_tab/models.py:408 msgid "JCOP30" msgstr "JCOP30" -#: amelie/personal_tab/models.py:373 +#: amelie/personal_tab/models.py:409 msgid "Shanghai Transport" msgstr "Shanghai Transport" -#: amelie/personal_tab/models.py:374 +#: amelie/personal_tab/models.py:410 msgid "MPCOS Combi" msgstr "MPCOS Combi" -#: amelie/personal_tab/models.py:375 +#: amelie/personal_tab/models.py:411 msgid "ISO Type B, Calypso" msgstr "ISO Type B, Calypso" -#: amelie/personal_tab/models.py:376 +#: amelie/personal_tab/models.py:412 msgid "ASK CTS256B, Type B" msgstr "ASK CTS256B, Type B" -#: amelie/personal_tab/models.py:377 +#: amelie/personal_tab/models.py:413 msgid "ASK CTS521B, Type B" msgstr "ASK CTS521B, Type B" -#: amelie/personal_tab/models.py:381 +#: amelie/personal_tab/models.py:417 msgid "Rfid code" msgstr "RFID-code" -#: amelie/personal_tab/models.py:383 +#: amelie/personal_tab/models.py:419 msgid "Date last used" msgstr "Datum laatst gebruikt" -#: amelie/personal_tab/models.py:384 +#: amelie/personal_tab/models.py:420 msgid "Creation date" msgstr "Aanmaakdatum" -#: amelie/personal_tab/models.py:388 +#: amelie/personal_tab/models.py:424 msgid "RFID card" msgstr "RFID-kaart" -#: amelie/personal_tab/models.py:389 +#: amelie/personal_tab/models.py:425 #: amelie/personal_tab/templates/info/rfid_cards.html:4 msgid "RFID cards" msgstr "Rfid-kaarten" -#: amelie/personal_tab/models.py:423 +#: amelie/personal_tab/models.py:459 msgid "Only alphanumerical signs are allowed" msgstr "Alleen alfanumerieke tekens toegestaan" -#: amelie/personal_tab/models.py:436 +#: amelie/personal_tab/models.py:472 msgid "text" msgstr "tekst" -#: amelie/personal_tab/models.py:439 +#: amelie/personal_tab/models.py:475 msgid "text (en)" msgstr "tekst (en)" -#: amelie/personal_tab/models.py:442 +#: amelie/personal_tab/models.py:478 msgid "active" msgstr "actief" -#: amelie/personal_tab/models.py:445 +#: amelie/personal_tab/models.py:481 msgid "membership fee" msgstr "contributie" -#: amelie/personal_tab/models.py:448 +#: amelie/personal_tab/models.py:484 msgid "consumptions" msgstr "consumpties" -#: amelie/personal_tab/models.py:451 +#: amelie/personal_tab/models.py:487 msgid "activities" msgstr "activiteiten" -#: amelie/personal_tab/models.py:454 +#: amelie/personal_tab/models.py:490 msgid "other payments" msgstr "overige betalingen" -#: amelie/personal_tab/models.py:479 +#: amelie/personal_tab/models.py:515 msgid "type of mandate" msgstr "machtigingsoort" -#: amelie/personal_tab/models.py:480 +#: amelie/personal_tab/models.py:516 msgid "types of mandate" msgstr "machtigingsoorten" -#: amelie/personal_tab/models.py:497 +#: amelie/personal_tab/models.py:533 msgid "type" msgstr "type" -#: amelie/personal_tab/models.py:509 +#: amelie/personal_tab/models.py:545 msgid "account holder" msgstr "Rekeninghouder" -#: amelie/personal_tab/models.py:513 +#: amelie/personal_tab/models.py:549 msgid "startdate" msgstr "begindatum" -#: amelie/personal_tab/models.py:516 +#: amelie/personal_tab/models.py:552 msgid "enddate" msgstr "einddatum" -#: amelie/personal_tab/models.py:519 +#: amelie/personal_tab/models.py:555 msgid "has been signed" msgstr "is getekend" -#: amelie/personal_tab/models.py:529 amelie/personal_tab/models.py:671 -#: amelie/personal_tab/models.py:827 +#: amelie/personal_tab/models.py:565 amelie/personal_tab/models.py:707 +#: amelie/personal_tab/models.py:915 msgid "mandate" msgstr "machtiging" -#: amelie/personal_tab/models.py:530 +#: amelie/personal_tab/models.py:566 msgid "mandates" msgstr "machtigingen" -#: amelie/personal_tab/models.py:677 +#: amelie/personal_tab/models.py:713 msgid "previous IBAN" msgstr "vorige IBAN" -#: amelie/personal_tab/models.py:680 +#: amelie/personal_tab/models.py:716 msgid "previous BIC" msgstr "vorige BIC" -#: amelie/personal_tab/models.py:683 +#: amelie/personal_tab/models.py:719 msgid "other bank" msgstr "andere bank" -#: amelie/personal_tab/models.py:686 +#: amelie/personal_tab/models.py:722 msgid "remark" msgstr "opmerking" -#: amelie/personal_tab/models.py:690 +#: amelie/personal_tab/models.py:726 #, python-format msgid "Amendment of %(authorization)s on %(date)s" msgstr "Amendement van %(authorization)s op %(date)s" -#: amelie/personal_tab/models.py:695 amelie/personal_tab/models.py:840 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:33 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:31 +#: amelie/personal_tab/models.py:731 amelie/personal_tab/models.py:922 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:43 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:41 msgid "amendment" msgstr "amendement" -#: amelie/personal_tab/models.py:696 +#: amelie/personal_tab/models.py:732 msgid "amendments" msgstr "amendementen" -#: amelie/personal_tab/models.py:713 +#: amelie/personal_tab/models.py:749 msgid "created on" msgstr "aangemaakt op" -#: amelie/personal_tab/models.py:742 +#: amelie/personal_tab/models.py:778 msgid "Direct withdrawal-task" msgstr "Incasso-opdracht" -#: amelie/personal_tab/models.py:743 +#: amelie/personal_tab/models.py:779 #: amelie/personal_tab/templates/debt_collection/list.html:4 #: amelie/personal_tab/templates/debt_collection/list.html:8 msgid "Direct withdrawal-tasks" msgstr "Incasso-opdrachten" -#: amelie/personal_tab/models.py:756 +#: amelie/personal_tab/models.py:792 msgid "Processed" msgstr "Verwerkt" -#: amelie/personal_tab/models.py:757 +#: amelie/personal_tab/models.py:793 msgid "Declined" msgstr "Geweigerd" -#: amelie/personal_tab/models.py:758 +#: amelie/personal_tab/models.py:794 msgid "Cancelled" msgstr "Geannuleerd" -#: amelie/personal_tab/models.py:762 +#: amelie/personal_tab/models.py:798 #: amelie/personal_tab/templates/debt_collection/new.html:49 #: amelie/personal_tab/templates/debt_collection/new.html:105 #: amelie/personal_tab/templates/debt_collection/new.html:114 msgid "First collection" msgstr "Eerste incasso" -#: amelie/personal_tab/models.py:763 +#: amelie/personal_tab/models.py:799 msgid "Continuation of direct withdrawal" msgstr "Vervolgincasso" -#: amelie/personal_tab/models.py:764 +#: amelie/personal_tab/models.py:800 msgid "Last collection" msgstr "Laatste incasso" -#: amelie/personal_tab/models.py:765 +#: amelie/personal_tab/models.py:801 msgid "One-time direct withdrawal" msgstr "Eenmalige incasso" -#: amelie/personal_tab/models.py:772 +#: amelie/personal_tab/models.py:808 msgid "assignment" msgstr "opdracht" -#: amelie/personal_tab/models.py:775 +#: amelie/personal_tab/models.py:811 msgid "date of execution" msgstr "uitvoerdatum" -#: amelie/personal_tab/models.py:778 +#: amelie/personal_tab/models.py:814 msgid "sequence type" msgstr "sequence type" -#: amelie/personal_tab/models.py:781 +#: amelie/personal_tab/models.py:817 msgid "status" msgstr "status" -#: amelie/personal_tab/models.py:804 +#: amelie/personal_tab/models.py:840 msgid "direct withdrawal-batch" msgstr "incasso-batch" -#: amelie/personal_tab/models.py:805 +#: amelie/personal_tab/models.py:841 msgid "direct withdrawal-batches" msgstr "incasso-batches" -#: amelie/personal_tab/models.py:823 +#: amelie/personal_tab/models.py:892 +#, fuzzy +#| msgid "Personal tab statistics" +msgid "personal tab settlement" +msgstr "Streeplijststatistieken" + +#: amelie/personal_tab/models.py:893 +#, fuzzy +#| msgid "Personal tab statistics" +msgid "personal tab settlements" +msgstr "Streeplijststatistieken" + +#: amelie/personal_tab/models.py:911 msgid "batch" msgstr "batch" -#: amelie/personal_tab/models.py:830 -#: amelie/personal_tab/templates/authorization/view.html:136 +#: amelie/personal_tab/models.py:918 +#: amelie/personal_tab/templates/authorization/view.html:141 #: amelie/personal_tab/templates/debt_collection/instruction_view.html:18 #: amelie/personal_tab/templates/debt_collection/process_batch.html:50 #: amelie/personal_tab/templates/debt_collection/view.html:93 msgid "end-to-end-id" msgstr "End-to-end-id" -#: amelie/personal_tab/models.py:857 +#: amelie/personal_tab/models.py:948 msgid "direct withdrawal-instruction" msgstr "incasso-instructie" -#: amelie/personal_tab/models.py:858 +#: amelie/personal_tab/models.py:949 msgid "direct withdrawal-instructions" msgstr "incasso-instructies" -#: amelie/personal_tab/models.py:903 +#: amelie/personal_tab/models.py:956 +#, fuzzy +#| msgid "Name (en)" +msgid "Name (NL)" +msgstr "Naam (en)" + +#: amelie/personal_tab/models.py:957 +#, fuzzy +#| msgid "Name (en)" +msgid "Name (EN)" +msgstr "Naam (en)" + +#: amelie/personal_tab/models.py:958 +#, fuzzy +#| msgid "Description (en)" +msgid "Description (NL)" +msgstr "Omschrijving (en)" + +#: amelie/personal_tab/models.py:959 +#, fuzzy +#| msgid "Description (en)" +msgid "Description (EN)" +msgstr "Omschrijving (en)" + +#: amelie/personal_tab/models.py:960 +msgid "This payment method will be visible when creating a manual payment directly in someone's personal tab." +msgstr "" + +#: amelie/personal_tab/models.py:961 +#, fuzzy +#| msgid "No membership" +msgid "Visible for memberships" +msgstr "Geen lidmaatschap" + +#: amelie/personal_tab/models.py:961 +msgid "This payment method will be visible when privileged members enter a manual payment for a membership." +msgstr "" + +#: amelie/personal_tab/models.py:962 +#, fuzzy +#| msgid "iCal all activities" +msgid "Visible for activities" +msgstr "iCal alle activiteiten" + +#: amelie/personal_tab/models.py:962 +msgid "This payment method will be visible when privileged members enter a manual payment for an activity." +msgstr "" + +#: amelie/personal_tab/models.py:963 +msgid "Icon name for frontend" +msgstr "" + +#: amelie/personal_tab/models.py:967 +#, fuzzy +#| msgid "Payment Method" +msgid "payment method" +msgstr "Betalingsmethode" + +#: amelie/personal_tab/models.py:968 +#, fuzzy +#| msgid "Payment Method" +msgid "payment methods" +msgstr "Betalingsmethode" + +#: amelie/personal_tab/models.py:995 +msgid "Frontend Icon" +msgstr "" + +#: amelie/personal_tab/models.py:1037 +#, fuzzy +#| msgid "payment" +msgid "payment date" +msgstr "betaling" + +#: amelie/personal_tab/models.py:1046 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:58 +#, fuzzy +#| msgid "Created on" +msgid "Created by" +msgstr "Aangemaakt op" + +#: amelie/personal_tab/models.py:1057 +#, fuzzy +#| msgid "pre-settlement" +msgid "manual payment settlement" +msgstr "pre-settlement" + +#: amelie/personal_tab/models.py:1058 +#, fuzzy +#| msgid "pre-settlement" +msgid "manual payment settlements" +msgstr "pre-settlement" + +#: amelie/personal_tab/models.py:1092 +msgid "Manual settlements can only be deleted in the same association year as they were paid in." +msgstr "" + +#: amelie/personal_tab/models.py:1107 +msgid "The extra balance that was created in this settlement was already used elsewhere." +msgstr "" + +#: amelie/personal_tab/models.py:1112 +#, python-brace-format +msgid "The total sum of the transactions ({transactions_sum}) does not inversely equal the linked manual payment's amount ({manual_amount})." +msgstr "" + +#: amelie/personal_tab/models.py:1117 +#, python-brace-format +msgid "The settlement's amount ({settlement_amount}) does not inversely equal the manual payment's amount and the extra balance ({manual_amount} + {extra_balance} = {total})." +msgstr "" + +#: amelie/personal_tab/models.py:1170 +#, python-brace-format +msgid "Deposit to personal tab balance on {date} for {name}" +msgstr "" + +#: amelie/personal_tab/models.py:1174 +#, python-brace-format +msgid "Refund of personal tab balance on {date} for {name}" +msgstr "" + +#: amelie/personal_tab/models.py:1178 +#, python-brace-format +msgid "Leftover balance after settlement on {date} for {name}" +msgstr "" + +#: amelie/personal_tab/models.py:1268 msgid "instruction" msgstr "instructie" -#: amelie/personal_tab/models.py:906 +#: amelie/personal_tab/models.py:1271 msgid "Processing date" msgstr "Verwerkingsdatum" -#: amelie/personal_tab/models.py:907 +#: amelie/personal_tab/models.py:1272 msgid "Fill out processing date, not rent date. See Rabobank Internet Banking." msgstr "Vul hier de verwerkingsdatum in, niet de rentedatum. Zie Rabobank Internetbankieren." -#: amelie/personal_tab/models.py:910 +#: amelie/personal_tab/models.py:1275 msgid "pre-settlement" msgstr "pre-settlement" -#: amelie/personal_tab/models.py:911 +#: amelie/personal_tab/models.py:1276 msgid "A debit reversal is pre-settlement when the withdrawal happens before the crediting of the direct withdrawal has taken place." msgstr "Een terugboeking is pre-settlement als de afboeking gebeurt voordat de bijschrijving van de incasso plaatsvindt." -#: amelie/personal_tab/models.py:915 +#: amelie/personal_tab/models.py:1280 msgid "reason" msgstr "reden" -#: amelie/personal_tab/models.py:918 +#: amelie/personal_tab/models.py:1283 #, python-format msgid "Debit reversal %s" msgstr "Terugboeking %s" -#: amelie/personal_tab/models.py:922 amelie/personal_tab/models.py:946 +#: amelie/personal_tab/models.py:1291 amelie/personal_tab/models.py:1315 msgid "debit reversal" msgstr "terugboeking" -#: amelie/personal_tab/models.py:923 +#: amelie/personal_tab/models.py:1292 msgid "reversal" msgstr "terugboeking" -#: amelie/personal_tab/models.py:962 +#: amelie/personal_tab/models.py:1330 +#, fuzzy +#| msgid "Date last used" +msgid "Date added" +msgstr "Datum laatst gebruikt" + +#: amelie/personal_tab/models.py:1331 +#, fuzzy +#| msgid "Edit reversal" +msgid "First reversal" +msgstr "Terugboeking wijzigen" + +#: amelie/personal_tab/models.py:1332 +#, fuzzy +#| msgid "Insert reversal" +msgid "Last reversal" +msgstr "Terugboeking invoeren" + +#: amelie/personal_tab/models.py:1360 +#, fuzzy +#| msgid "unknown id" +msgid "known bad BIC" +msgstr "unknown id" + +#: amelie/personal_tab/models.py:1361 +msgid "known bad BICs" +msgstr "" + +#: amelie/personal_tab/models.py:1374 #: amelie/personal_tab/templates/print/log_list.html:30 #: amelie/personal_tab/templates/print/refund.html:26 msgid "Actor" msgstr "Acteur" -#: amelie/personal_tab/models.py:974 +#: amelie/personal_tab/models.py:1386 msgid "Document name" msgstr "Documentnaam" -#: amelie/personal_tab/models.py:977 +#: amelie/personal_tab/models.py:1389 msgid "Printer Job ID" msgstr "Printer Opdracht-ID" -#: amelie/personal_tab/models.py:980 +#: amelie/personal_tab/models.py:1392 msgid "Page count" msgstr "Aantal pagina's" -#: amelie/personal_tab/models.py:993 +#: amelie/personal_tab/models.py:1405 msgid "print log entry" msgstr "printlogboekvermelding" -#: amelie/personal_tab/models.py:994 +#: amelie/personal_tab/models.py:1406 msgid "print log entries" msgstr "printlogboekvermeldingen" -#: amelie/personal_tab/models.py:997 +#: amelie/personal_tab/models.py:1409 #, python-format msgid "%(actor)s printed %(pages)s page(s) %(for_str)s" msgstr "%(actor)s heeft %(pages)s pagina(s) geprint %(for_str)s" -#: amelie/personal_tab/models.py:1001 +#: amelie/personal_tab/models.py:1413 #, python-format msgid "for %(committee)s" msgstr "voor %(committee)s" -#: amelie/personal_tab/models.py:1003 +#: amelie/personal_tab/models.py:1415 msgid "for personal use" msgstr "voor persoonlijk gebruik" +#: amelie/personal_tab/models.py:1456 +msgid "bookkeeping" +msgstr "" + #: amelie/personal_tab/pos_models.py:21 msgid "Login token" msgstr "Logintoken" @@ -11595,6 +12617,8 @@ msgstr "Uitsluiten toegankelijk voor de streeplijst." #: amelie/personal_tab/pos_views.py:83 amelie/personal_tab/pos_views.py:593 #: amelie/personal_tab/pos_views.py:604 +#: amelie/personal_tab/register_views.py:60 +#: amelie/personal_tab/register_views.py:66 msgid "Something went wrong, please try again later." msgstr "Er ging iets fout, probeer het later opnieuw." @@ -11603,10 +12627,12 @@ msgid "No cards were scanned, please try again." msgstr "Er zijn geen kaarten gescand, probeer het opnieuw." #: amelie/personal_tab/pos_views.py:108 amelie/personal_tab/pos_views.py:227 +#: amelie/personal_tab/register_views.py:85 msgid "You do not have a valid mandate or are not a member anymore. Contact the board for help." msgstr "Je hebt geen geldige machtiging of bent geen lid meer. Neem contact op met het bestuur voor hulp." #: amelie/personal_tab/pos_views.py:111 +#: amelie/personal_tab/register_views.py:88 msgid "RFID-cards from multiple people recognised. Please scan one card at a time." msgstr "RFID-kaarten van meerdere personen herkend. Scan alsjeblieft maar 1 kaart tegelijkertijd." @@ -11623,26 +12649,32 @@ msgid "Invalid token type requested!" msgstr "Ongeldig token type opgevraagd!" #: amelie/personal_tab/pos_views.py:174 +#: amelie/personal_tab/register_views.py:119 msgid "No card was scanned, please try again." msgstr "Er is geen kaart gescand, probeer het opnieuw." #: amelie/personal_tab/pos_views.py:181 +#: amelie/personal_tab/register_views.py:126 msgid "Something went wrong while scanning the RFID card. Please try again." msgstr "Er ging iets mis bij het scannen van de RFID-kaart. Probeer het opnieuw." #: amelie/personal_tab/pos_views.py:188 amelie/personal_tab/pos_views.py:617 +#: amelie/personal_tab/register_views.py:133 msgid "This card is already registered." msgstr "Deze kaart is al geregistreerd." #: amelie/personal_tab/pos_views.py:190 +#: amelie/personal_tab/register_views.py:135 msgid "This card has already been registered, but has not been activated on the website. Please contact the board." msgstr "Deze kaart is al geregistreerd, maar nog niet geactiveerd via de website. Neem contact op met het bestuur." #: amelie/personal_tab/pos_views.py:210 +#: amelie/personal_tab/register_views.py:155 msgid "This user is inactive. Please contact the board." msgstr "Deze gebruiker is inactief. Neem contact op met het bestuur." -#: amelie/personal_tab/pos_views.py:214 amelie/personal_tab/register.py:33 +#: amelie/personal_tab/pos_views.py:214 +#: amelie/personal_tab/register_views.py:159 msgid "You are (not yet/ no longer) a member of Inter-Actief. Ask the board for a membership form." msgstr "Je bent (nog) geen lid (meer) van Inter-Actief. Vraag het bestuur om een lidmaatschapsformulier." @@ -11651,14 +12683,18 @@ msgid "Card successfully registered. Please scan the card again to log in." msgstr "Kaart succesvol geregistreerd. Scan de kaart opnieuw om in te loggen." #: amelie/personal_tab/pos_views.py:265 +#: amelie/personal_tab/register_views.py:191 msgid "Invalid login token, please try again!" msgstr "Ongeldig login token, probeer het opnieuw!" #: amelie/personal_tab/pos_views.py:278 amelie/personal_tab/pos_views.py:281 +#: amelie/personal_tab/register_views.py:204 +#: amelie/personal_tab/register_views.py:207 msgid "Invalid token, please try again!" msgstr "Ongeldig token, probeer het opnieuw!" #: amelie/personal_tab/pos_views.py:285 +#: amelie/personal_tab/register_views.py:211 msgid "This token was already used before, please try again!" msgstr "Dit token is al een keer eerder gebruikt, probeer het opnieuw!" @@ -11667,6 +12703,7 @@ msgid "Cookie Corner login verified successfully!" msgstr "Koekenkast login succesvol geverifiëerd!" #: amelie/personal_tab/pos_views.py:293 +#: amelie/personal_tab/register_views.py:217 msgid "New card registration verified successfully!" msgstr "Nieuwe kaartregistratie succesvol geverifiëerd!" @@ -11675,11 +12712,15 @@ msgid "Unknown token verified successfully!" msgstr "Onbekend token succesvol geverifiëerd!" #: amelie/personal_tab/pos_views.py:299 +#: amelie/personal_tab/register_views.py:221 msgid "An unknown error occurred, please try again!" msgstr "Er is een onbekende fout opgetreden, probeer het opnieuw!" #: amelie/personal_tab/pos_views.py:311 -msgid "The login code is invalid or expired, please try again!." +#: amelie/personal_tab/register_views.py:233 +#, fuzzy +#| msgid "The login code is invalid or expired, please try again!." +msgid "The login code is invalid or expired, please try again!" msgstr "Deze logincode is ongeldig of verlopen, probeer het opnieuw!" #: amelie/personal_tab/pos_views.py:354 @@ -11746,90 +12787,90 @@ msgstr "Kaart succesvol geregistreerd. Je bent uitgelogd." msgid "Multiple unknown RFID-cards detected. Please scan one card at a time. You have been logged out." msgstr "Meerdere onbekende RFID-kaarten gescand. Scan alsjeblieft 1 kaart tegelijkertijd. Je bent uitgelogd." -#: amelie/personal_tab/print_views.py:75 +#: amelie/personal_tab/print_views.py:77 #, python-brace-format msgid "Document '{name}' with {pages} page(s) was queued for printing successfully." msgstr "Document '{name}' met {pages} pagina(s) is succesvol in de printwachtrij gezet." -#: amelie/personal_tab/print_views.py:81 +#: amelie/personal_tab/print_views.py:83 #, python-brace-format msgid "Error while submitting print to the printer. No prints were registered. Error: {ex}" msgstr "Fout tijdens het versturen van de printopdracht naar de printer. Er zijn geen prints geregistreerd. Error: {ex}" -#: amelie/personal_tab/print_views.py:101 +#: amelie/personal_tab/print_views.py:103 #, python-brace-format msgid "Refund for print, transaction #{tid}" msgstr "Terugbetaling voor print, transactie #{tid}" -#: amelie/personal_tab/print_views.py:106 +#: amelie/personal_tab/print_views.py:128 #, python-brace-format msgid "Print by {person} was refunded successfully." msgstr "Print van {person} is succesvol terugbetaald." -#: amelie/personal_tab/register.py:22 -msgid "Username and password are obligatory" -msgstr "Gebruikersnaam en wachtwoord zijn verplicht." - -#: amelie/personal_tab/register.py:29 -msgid "Username and/or password incorrect." -msgstr "Gebruikersnaam en/of wachtwoord is onjuist." - -#: amelie/personal_tab/register.py:76 -msgid "No cards are scanned." -msgstr "Er zijn geen kaarten gescand." - -#: amelie/personal_tab/register.py:85 -msgid "This RFID card is already registered." -msgstr "Deze RFID-kaart is al geregistreerd." +#: amelie/personal_tab/register_views.py:81 +#, fuzzy +#| msgid "This card is already registered." +msgid "This card has already been registered." +msgstr "Deze kaart is al geregistreerd." -#: amelie/personal_tab/register.py:91 -msgid "Card is registered." +#: amelie/personal_tab/register_views.py:165 +#, fuzzy +#| msgid "Card is registered." +msgid "Card successfully registered." msgstr "Kaart is geregistreerd." -#: amelie/personal_tab/statistics.py:276 +#: amelie/personal_tab/statistics.py:280 msgid "{} ledger statistics" msgstr "{} grootboekstatistieken" -#: amelie/personal_tab/statistics.py:283 +#: amelie/personal_tab/statistics.py:287 #: amelie/personal_tab/templates/statistics/statistics.html:48 #: amelie/personal_tab/templates/statistics/statistics_form.html:4 #: amelie/personal_tab/templates/statistics/statistics_form.html:9 msgid "Personal tab statistics" msgstr "Streeplijststatistieken" -#: amelie/personal_tab/statistics.py:284 +#: amelie/personal_tab/statistics.py:288 #: amelie/personal_tab/templates/balance/balance_form.html:4 #: amelie/personal_tab/templates/balance/balance_form.html:9 #: amelie/personal_tab/templates/statistics/statistics.html:96 msgid "Personal tab balance" msgstr "Streeplijst totalen" -#: amelie/personal_tab/statistics.py:286 +#: amelie/personal_tab/statistics.py:290 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:81 #: amelie/personal_tab/templates/statistics/statistics.html:231 msgid "Alexia transactions" msgstr "Alexiatransacties" -#: amelie/personal_tab/statistics.py:287 +#: amelie/personal_tab/statistics.py:291 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:78 msgid "Contribution transactions" msgstr "Contributietransacties" -#: amelie/personal_tab/statistics.py:288 -#: amelie/personal_tab/templates/statistics/statistics.html:403 +#: amelie/personal_tab/statistics.py:292 +#: amelie/personal_tab/templates/statistics/statistics.html:401 msgid "Discount offers" msgstr "Kortingsacties" -#: amelie/personal_tab/statistics.py:289 -#: amelie/personal_tab/templates/statistics/statistics.html:369 +#: amelie/personal_tab/statistics.py:293 +#: amelie/personal_tab/templates/statistics/statistics.html:367 msgid "Discount balances" msgstr "Kortingstegoeden" -#: amelie/personal_tab/statistics.py:290 +#: amelie/personal_tab/statistics.py:294 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:82 msgid "Custom transactions" msgstr "Overige transacties" +#: amelie/personal_tab/statistics.py:295 +#: amelie/personal_tab/templates/wrapped/wrapped.html:129 +msgid "Totals" +msgstr "Totalen" + #: amelie/personal_tab/templates/authorization/amendment.html:4 #: amelie/personal_tab/templates/authorization/amendment.html:12 -#: amelie/personal_tab/templates/authorization/view.html:113 +#: amelie/personal_tab/templates/authorization/view.html:118 msgid "Add amendment" msgstr "Amendement invoeren" @@ -11914,9 +12955,40 @@ msgstr "Machtigingen zoeken" msgid "Person/account holder" msgstr "Persoon/rekeninghouder" -#: amelie/personal_tab/templates/authorization/list.html:103 +#: amelie/personal_tab/templates/authorization/list.html:92 +#: amelie/personal_tab/templates/authorization/view.html:28 +#: amelie/personal_tab/templates/balance/balance.html:48 +#: amelie/personal_tab/templates/balance/balance.html:81 +#: amelie/personal_tab/templates/balance/balance.html:119 +#: amelie/personal_tab/templates/balance/balance.html:152 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:26 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:58 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:30 +#: amelie/personal_tab/templates/debt_collection/process_batch.html:69 +#: amelie/personal_tab/templates/debt_collection/view.html:112 +#: amelie/personal_tab/templates/info/transaction_information.html:43 +#: amelie/personal_tab/templates/lists/activity_transactions.html:30 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:28 +#: amelie/personal_tab/templates/lists/authorization_select.html:39 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:28 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:31 +#: amelie/personal_tab/templates/lists/custom_transactions.html:28 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:30 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:29 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:28 +#: amelie/personal_tab/templates/lists/payment_transactions.html:31 +#: amelie/personal_tab/templates/lists/transaction_row.html:15 +#: amelie/personal_tab/templates/print/log_list.html:56 +#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:29 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:49 +#, fuzzy, python-format +#| msgid "No transactions on personal tab." +msgid "Go to %(name)s's personal tab" +msgstr "Geen streeplijsttransacties." + +#: amelie/personal_tab/templates/authorization/list.html:108 #: amelie/personal_tab/templates/info/account_information.html:40 -#: amelie/personal_tab/templates/lists/authorization_select.html:44 +#: amelie/personal_tab/templates/lists/authorization_select.html:49 msgid "(not yet) active" msgstr "(nog) niet actief" @@ -11939,68 +13011,68 @@ msgstr "Machtigingen beëindigen" msgid "Mandate reference" msgstr "Machtigingskenmerk" -#: amelie/personal_tab/templates/authorization/view.html:54 +#: amelie/personal_tab/templates/authorization/view.html:59 msgid "print" msgstr "afdrukken" -#: amelie/personal_tab/templates/authorization/view.html:59 +#: amelie/personal_tab/templates/authorization/view.html:64 msgid "Amendments" msgstr "Amendementen" -#: amelie/personal_tab/templates/authorization/view.html:67 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:126 +#: amelie/personal_tab/templates/authorization/view.html:72 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:131 msgid "Previous IBAN" msgstr "Vorige IBAN" -#: amelie/personal_tab/templates/authorization/view.html:68 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:130 +#: amelie/personal_tab/templates/authorization/view.html:73 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:135 msgid "Previous BIC" msgstr "Vorige BIC" -#: amelie/personal_tab/templates/authorization/view.html:69 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:134 +#: amelie/personal_tab/templates/authorization/view.html:74 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:139 msgid "Other bank" msgstr "Andere bank" -#: amelie/personal_tab/templates/authorization/view.html:71 +#: amelie/personal_tab/templates/authorization/view.html:76 msgid "Instruction" msgstr "Instructie" -#: amelie/personal_tab/templates/authorization/view.html:110 +#: amelie/personal_tab/templates/authorization/view.html:115 msgid "No amendments have been found." msgstr "Er zijn geen amendementen gevonden." -#: amelie/personal_tab/templates/authorization/view.html:116 +#: amelie/personal_tab/templates/authorization/view.html:121 msgid "Current amendment has not yet been sent to the bank, so a new one cannot be added." msgstr "Het huidige amendement is nog niet naar de bank verstuurd, dus er kan nog geen nieuwe toegevoegd worden." -#: amelie/personal_tab/templates/authorization/view.html:118 +#: amelie/personal_tab/templates/authorization/view.html:123 msgid "This mandate is not signed, so a new amendment cannot be added." msgstr "Deze machtiging is niet ondertekend, dus er kan geen amendement aan worden toegevoegd." -#: amelie/personal_tab/templates/authorization/view.html:120 +#: amelie/personal_tab/templates/authorization/view.html:125 msgid "This mandate is terminated, so a new amendment cannot be added." msgstr "Deze machtiging is beëindigd, dus er kan geen amendenemt aan worden toegevoegd." -#: amelie/personal_tab/templates/authorization/view.html:127 +#: amelie/personal_tab/templates/authorization/view.html:132 msgid "Instructions" msgstr "Instructies" -#: amelie/personal_tab/templates/authorization/view.html:135 +#: amelie/personal_tab/templates/authorization/view.html:140 msgid "Task/batch" msgstr "Opdracht/batch" -#: amelie/personal_tab/templates/authorization/view.html:138 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:75 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:117 +#: amelie/personal_tab/templates/authorization/view.html:143 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:80 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:122 #: amelie/personal_tab/templates/debt_collection/process_batch.html:51 #: amelie/personal_tab/templates/debt_collection/view.html:94 msgid "Amendment" msgstr "Amendement" -#: amelie/personal_tab/templates/authorization/view.html:172 -#: amelie/personal_tab/templates/debt_collection/process_batch.html:95 -#: amelie/personal_tab/templates/debt_collection/view.html:138 +#: amelie/personal_tab/templates/authorization/view.html:177 +#: amelie/personal_tab/templates/debt_collection/process_batch.html:100 +#: amelie/personal_tab/templates/debt_collection/view.html:143 msgid "No instructions have been found." msgstr "Er zijn geen instructies gevonden." @@ -12022,21 +13094,22 @@ msgid "Total costs" msgstr "Totaal bedrag" #: amelie/personal_tab/templates/balance/balance.html:20 -#: amelie/personal_tab/templates/balance/balance.html:82 +#: amelie/personal_tab/templates/balance/balance.html:100 msgid "Exam credits" msgstr "Tentamenkoekentegoed" #: amelie/personal_tab/templates/balance/balance.html:36 -#: amelie/personal_tab/templates/balance/balance.html:89 +#: amelie/personal_tab/templates/balance/balance.html:107 msgid "Former Member" msgstr "Voormalige leden" #: amelie/personal_tab/templates/balance/balance.html:37 -#: amelie/personal_tab/templates/balance/balance.html:61 -#: amelie/personal_tab/templates/balance/balance.html:90 -#: amelie/personal_tab/templates/balance/balance.html:116 +#: amelie/personal_tab/templates/balance/balance.html:70 +#: amelie/personal_tab/templates/balance/balance.html:108 +#: amelie/personal_tab/templates/balance/balance.html:141 #: amelie/personal_tab/templates/balance/balance_form.html:20 #: amelie/personal_tab/templates/cookie_corner_overview.html:20 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:50 msgid "Balance" msgstr "Saldo" @@ -12077,13 +13150,25 @@ msgid "Cookie and candy list of %(person)s" msgstr "Streeplijst van %(person)s" #: amelie/personal_tab/templates/cookie_corner_dashboard.html:13 -#, python-format -msgid "Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s." +#, fuzzy, python-format +#| msgid "Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s." +msgid "" +"\n" +" Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s.\n" +" " msgstr "" "Je huidige streeplijstsaldo is € %(curr_balance)s,\n" " met toekomstige transacties meegerekend is dat € %(all_balance)s." -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:16 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:17 +msgid "" +"\n" +" If you signed a direct debit mandate, the balance is usually settled by withdrawing it from your bank account periodically.\n" +" If you do not have a mandate, you can settle any outstanding balance with a manual payment. To arrange such a payment, please contact the board.\n" +" " +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:24 #, python-format msgid "" "\n" @@ -12093,7 +13178,7 @@ msgstr "" "\n" "Er wordt op %(date)s € %(amount)s geincasseerd van je rekening." -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:23 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:31 #, python-format msgid "" "\n" @@ -12104,13 +13189,13 @@ msgstr "" " Omdat je tentamens hebt ingeleverd, mag je nog voor € %(credit)s aan consumpties kopen.\n" " " -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:38 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:46 #: amelie/personal_tab/templates/cookie_corner_transactions.html:37 #: amelie/personal_tab/templates/exam_cookie_credit/overview.html:26 msgid "No transactions have been found" msgstr "Er zijn geen transacties gevonden." -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:41 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:49 #, python-format msgid "" "\n" @@ -12120,35 +13205,114 @@ msgstr "" "\n" "Laatste %(amount)s toegevoegde transacties." -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:49 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:57 #: amelie/personal_tab/templates/exam_cookie_credit/overview.html:45 msgid "Previous transactions" msgstr "Eerdere transacties" -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:52 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:60 +#: amelie/personal_tab/templates/cookie_corner_overview.html:30 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:54 +#, fuzzy +#| msgid "Usage transactions" +msgid "Unpaid transactions" +msgstr "Verbruiktransacties" + +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:63 msgid "History of exam credit" msgstr "Geschiedenis tentamentegoed" -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:55 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:66 #: amelie/personal_tab/templates/wrapped/wrapped.html:4 #: amelie/personal_tab/templates/wrapped/wrapped_no_transactions.html:4 msgid "Cookie Corner Wrapped" msgstr "Cookie Corner Wrapped" -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:63 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:72 +#, fuzzy +#| msgid "Board tasks" +msgid "Board tasks:" +msgstr "Bestuurstaken" + +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:74 msgid "Add transaction to personal tab" msgstr "Streeplijsttransactie toevoegen" -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:66 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:77 msgid "Add other transaction" msgstr "Overige transactie toevoegen" -#: amelie/personal_tab/templates/cookie_corner_dashboard.html:69 +#: amelie/personal_tab/templates/cookie_corner_dashboard.html:80 #: amelie/personal_tab/templates/exam_cookie_credit/add.html:5 #: amelie/personal_tab/templates/exam_cookie_credit/overview.html:54 msgid "Add credit for exams" msgstr "Tentamenkoekentegoed toevoegen" +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:4 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:8 +msgid "Manual Payment Settlement" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:14 +msgid "Reference of manual payment" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:37 +#, fuzzy +#| msgid "way of payment" +msgid "Date of payment" +msgstr "betalingswijze" + +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:41 +msgid "Method of payment" +msgstr "Betaalwijze" + +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:79 +#: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:84 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:4 +#, fuzzy +#| msgid "Delete payment" +msgid "Delete manual payment" +msgstr "Verwijder betaling" + +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:4 +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:8 +#, python-format +msgid "Manual payments - Page %(page)s/%(total)s" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:12 +msgid "" +"\n" +" These are payments that people made manually directly to the association, for example, by cash or by PIN.\n" +" A manual payment can pay for transactions that were made against their personal tab, or it can add\n" +" a negative amount to their personal tab to create a credit they can use for activities.\n" +" " +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:19 +msgid "" +"\n" +" Click on a payment description to view details about which transactions the manual payment paid for,\n" +" and if there was any extra money deposited.\n" +" " +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:27 +msgid "" +"\n" +" To create a manual payment, go to the person's personal tab overview\n" +" and click on the \"Add manual payment\" button in the relevant block.\n" +" " +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:78 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:42 +#, fuzzy +#| msgid "No amendments have been found." +msgid "No manual payments have been found." +msgstr "Er zijn geen amendementen gevonden." + #: amelie/personal_tab/templates/cookie_corner_overview.html:13 msgid "Personal overview" msgstr "Persoonlijk overzicht" @@ -12174,9 +13338,158 @@ msgid "Direct withdrawal-tasks (SEPA)" msgstr "Incasso-opdrachten (SEPA)" #: amelie/personal_tab/templates/cookie_corner_overview.html:28 +#: amelie/personal_tab/templates/info/payments.html:59 +#, fuzzy +#| msgid "payments" +msgid "Manual payments" +msgstr "betalingen" + +#: amelie/personal_tab/templates/cookie_corner_overview.html:29 msgid "Unpaid memberships" msgstr "Onbetaalde lidmaatschappen" +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:8 +#, fuzzy, python-format +#| msgid "Delete transaction '%(object)s'" +msgid "Delete manual payment '%(object)s'" +msgstr "Verwijder transactie '%(object)s'" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:11 +#: amelie/personal_tab/templates/transactions/transaction_delete.html:11 +#, python-format +msgid "" +"\n" +" Are you sure you want to delete '%(object)s' for %(person)s?\n" +" " +msgstr "" +"\n" +"Weet je zeker dat je '%(object)s' van %(person)s wilt verwijderen?" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:4 +#, fuzzy, python-format +#| msgid "Direct debit for %(person)s" +msgid "Create manual payment for %(person)s" +msgstr "Incasso's voor %(person)s" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:9 +#, fuzzy, python-format +#| msgid "Direct debit for %(person)s" +msgid "New manual payment for %(person)s" +msgstr "Incasso's voor %(person)s" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:14 +#: amelie/personal_tab/templates/transactions/transaction_form.html:20 +msgid "You are now adding a transaction to a personal tab" +msgstr "Je voegt nu een streeplijsttransactie toe" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:16 +#: amelie/personal_tab/templates/transactions/transaction_form.html:22 +msgid "You are now adding a transaction in the category \"Other\"" +msgstr "Je voegt nu een overige transactie toe" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:32 +#, fuzzy +#| msgid "Total cost" +msgid "Total of selected" +msgstr "Totaalbedrag" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:33 +msgid "Amount being paid" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:34 +#, fuzzy +#| msgid "Personal tab balance" +msgid "Personal tab balance before payment" +msgstr "Streeplijst totalen" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:35 +#, fuzzy +#| msgid "Personal tab balance" +msgid "Personal tab balance after payment" +msgstr "Streeplijst totalen" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:38 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:40 +#, fuzzy +#| msgid "Today:" +msgid "Today" +msgstr "Vandaag:" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:39 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:41 +msgid "Including future" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:56 +#, fuzzy +#| msgid "" +#| "\n" +#| " One other person has filled in their availability.\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(count)s others have filled in their availability.\n" +#| " " +msgid "" +"\n" +" Please select the transactions that will be marked as paid with this settlement.\n" +" " +msgstr "" +"\n" +"Een persoon heeft al zijn beschikbaarheid aangegeven." + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:59 +#, fuzzy +#| msgid "" +#| "\n" +#| " You are not enrolled in this activity yet.\n" +#| " " +msgid "" +"\n" +" Rows marked in green are transactions that have a date in the future.\n" +" " +msgstr "" +"\n" +"Je doet nog niet mee met deze activiteit." + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:66 +msgid "There were errors while submitting the form." +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:76 +#, fuzzy +#| msgid "For more information, see:" +msgid "For more errors, see below." +msgstr "Zie voor meer informatie" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:79 +msgid "See below for the errors." +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:87 +msgid "Include?" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:90 +#, fuzzy +#| msgid "ID" +msgid "#ID" +msgstr "ID" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:121 +#, fuzzy +#| msgid "No transactions found" +msgid "No unpaid transactions found!" +msgstr "Geen transacties gevonden" + +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:4 +#: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:8 +#, fuzzy, python-format +#| msgid "Direct debit for %(person)s" +msgid "Manual payments for %(person)s" +msgstr "Incasso's voor %(person)s" + #: amelie/personal_tab/templates/cookie_corner_price_list.html:22 #: amelie/personal_tab/templates/cookie_corner_price_list.html:65 msgid "Price (€)" @@ -12218,13 +13531,283 @@ msgid "from %(date_from)s till %(date_to)s" msgstr "van %(date_from)s tot %(date_to)s" #: amelie/personal_tab/templates/cookie_corner_transactions.html:45 -#: amelie/personal_tab/templates/lists/transactions_totals.html:23 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:67 +#: amelie/personal_tab/templates/lists/transactions_totals.html:24 msgid "Alexia" msgstr "Alexia" +#: amelie/personal_tab/templates/cookie_corner_transactions.html:48 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:70 +#: amelie/personal_tab/templates/lists/transactions_totals.html:25 +#, fuzzy +#| msgid "instruction" +msgid "Contribution" +msgstr "instructie" + +#: amelie/personal_tab/templates/cookie_corner_transactions.html:54 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:45 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:76 +#: amelie/personal_tab/templates/info/payments.html:4 +#: amelie/personal_tab/templates/lists/transactions_totals.html:29 +msgid "Payments" +msgstr "Betalingen" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:5 +#, fuzzy +#| msgid "Transactions" +msgid "Unpaid Transactions" +msgstr "Transacties" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:9 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " No transactions found\n" +#| " " +msgid "" +"\n" +" Unpaid Transactions for %(name)s\n" +" " +msgstr "" +"\n" +"Geen transacties gevonden" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:15 +#, fuzzy, python-format +#| msgid "Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s." +msgid "" +"\n" +" Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s.\n" +" " +msgstr "" +"Je huidige streeplijstsaldo is € %(curr_balance)s,\n" +" met toekomstige transacties meegerekend is dat € %(all_balance)s." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:18 +#, fuzzy +#| msgid "Blame overview" +msgid "Back to overview" +msgstr "Blame-overzicht" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:26 +#: amelie/personal_tab/templates/info/transaction_information.html:3 +#, fuzzy +#| msgid "Transaction" +msgid "Transaction type" +msgstr "Transactie" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:39 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:30 +#: amelie/personal_tab/templates/pos/success.html:39 +msgid "Subtotal" +msgstr "Subtotaal" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:57 +#, fuzzy +#| msgid "Transactions" +msgid "Transactions per type" +msgstr "Transacties" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:59 +#, fuzzy +#| msgid "No transactions have been found" +msgid "No unpaid transactions have been found" +msgstr "Er zijn geen transacties gevonden." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:6 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:10 +#, python-format +msgid "Unpaid transactions - Page %(page)s/%(total)s" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:12 +#, fuzzy +#| msgid "There are people whose mandate has ended." +msgid "These are people with transactions that have not been paid for." +msgstr "Er zijn personen met een beëindigde machtiging." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:16 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Your membership type is %(type)s.\n" +#| " " +msgid "" +"\n" +" Only people without a mandate of type '%(filtered_type)s' are shown.\n" +" " +msgstr "" +"\n" +"Je lidmaatschapstype is %(type)s." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:20 +#, fuzzy +#| msgid "Only activities that you are allowed to edit are shown." +msgid "Only people without a mandate are shown." +msgstr "Alleen activiteiten die jij mag aanpassen worden getoond." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:26 +#, fuzzy +#| msgid "" +#| "\n" +#| " The following people have uploaded a new profile picture.\n" +#| " " +msgid "" +"\n" +" Only people with a mandate of type 'filtered_type' are shown.\n" +" " +msgstr "" +"\n" +" De volgende personen hebben een nieuwe profielfoto geupload.\n" +" " + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:30 +#, fuzzy +#| msgid "Only activities in the future are shown." +msgid "Only people with a mandate are shown." +msgstr "Alleen activiteiten in de toekomst worden getoond." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:35 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Last %(amount)s added transactions.\n" +#| " " +msgid "" +"\n" +" Only transactions of type '%(filter_transactions)s' are shown.\n" +" " +msgstr "" +"\n" +"Laatste %(amount)s toegevoegde transacties." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:40 +msgid "Each person can be expanded to view details about the individual transactions." +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:44 +msgid "Total number of people" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:48 +#, fuzzy +#| msgid "Number of transactions" +msgid "Total number of transactions" +msgstr "Aantal transacties" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:52 +#, fuzzy +#| msgid "Total costs" +msgid "Total sum of unpaid costs" +msgstr "Totaal bedrag" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:57 +#, fuzzy +#| msgid "Sister associations" +msgid "Filter options" +msgstr "Zusterverenigingen" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:60 +#, fuzzy +#| msgid "Enroll people" +msgid "Only people with:" +msgstr "Personen inschrijven" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:62 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:70 +#, fuzzy +#| msgid "Contribution transactions" +msgid "Contribution mandate" +msgstr "Contributietransacties" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:63 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:71 +#, fuzzy +#| msgid "Existing mandate" +msgid "Consumption mandate" +msgstr "Bestaande machtiging" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:64 +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:72 +#, fuzzy +#| msgid "End mandate" +msgid "Any mandate" +msgstr "Machtiging beëindigen" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:68 +msgid "Only people without:" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:76 +#, fuzzy +#| msgid "Last transaction of the year: " +msgid "Only transactions of type:" +msgstr "Laatste transactie van het jaar: " + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:79 +#, fuzzy +#| msgid "No activity transactions." +msgid "Activity transactions" +msgstr "Geen activiteittransacties." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:80 +#, fuzzy +#| msgid "No other transactions." +msgid "Cookie corner transactions" +msgstr "Geen overige transacties." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:83 +#, fuzzy +#| msgid "Usage transactions" +msgid "Reversal transactions" +msgstr "Verbruiktransacties" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:88 +#, fuzzy +#| msgid " files." +msgid "Clear filters" +msgstr " bestanden." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:91 +msgid "Toggle expand/collapse all" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:103 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Last %(count)s transactions\n" +#| " " +msgid "" +"\n" +" %(name)s - (%(num_transactions)s) - € %(total_cost)s\n" +" " +msgstr "" +"\n" +"Laatste %(count)s toegevoegde transacties." + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:112 +#, python-format +msgid "Go to %(name)s's personal tab (opens new tab)" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:115 +msgid "Create settlement (opens new tab)" +msgstr "" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:155 +#, fuzzy +#| msgid "No transactions" +msgid "No unpaid transactions" +msgstr "Geen transacties" + +#: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:157 +msgid "There are no people with unpaid transactions! Woah! 🎉🎉🎉" +msgstr "" + #: amelie/personal_tab/templates/debt_collection/instruction_reversal.html:4 #: amelie/personal_tab/templates/debt_collection/instruction_reversal.html:8 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:96 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:101 msgid "Insert reversal" msgstr "Terugboeking invoeren" @@ -12240,7 +13823,7 @@ msgstr "" #: amelie/personal_tab/templates/debt_collection/instruction_reversal_delete.html:4 #: amelie/personal_tab/templates/debt_collection/instruction_reversal_delete.html:9 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:107 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:112 msgid "Delete reversal" msgstr "Terugboeking verwijderen" @@ -12264,31 +13847,31 @@ msgstr "Incasso-instructie" msgid "Reference of direct withdrawal" msgstr "Incassoreferentie" -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:49 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:54 #: amelie/personal_tab/templates/debt_collection/process_batch.html:4 #: amelie/personal_tab/templates/debt_collection/view.html:4 #: amelie/personal_tab/templates/debt_collection/view.html:8 msgid "direct withdrawal-task" msgstr "incasso-opdracht" -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:57 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:62 msgid "Direct withdrawal-batch" msgstr "Incasso-batch" -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:80 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:85 msgid "no" msgstr "nee" -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:86 -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:149 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:91 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:154 msgid "Debit reversal" msgstr "Terugboeking" -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:102 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:107 msgid "Edit reversal" msgstr "Terugboeking wijzigen" -#: amelie/personal_tab/templates/debt_collection/instruction_view.html:158 +#: amelie/personal_tab/templates/debt_collection/instruction_view.html:163 msgid "Pre-settlement" msgstr "Pre-settlement" @@ -12466,16 +14049,18 @@ msgstr "Totaal gestorneerd" msgid "Export direct withdrawal-file" msgstr "Incassobestand exporteren" -#: amelie/personal_tab/templates/debt_collection/view.html:144 +#: amelie/personal_tab/templates/debt_collection/view.html:149 msgid "No batches" msgstr "Geen batches" -#: amelie/personal_tab/templates/debt_collection/view.html:146 +#: amelie/personal_tab/templates/debt_collection/view.html:151 msgid "No batches have been found." msgstr "Er zijn geen batches gevonden." #: amelie/personal_tab/templates/declaration/declaration.mail:3 -msgid "Declaration submitted via Amélie" +#, fuzzy, python-format +#| msgid "Declaration submitted via Amélie" +msgid "Declaration submitted via Amélie by %(name)s on %(date)s" msgstr "Declaratie verstuurd via Amélie" #: amelie/personal_tab/templates/declaration/declaration.mail:24 @@ -12596,24 +14181,24 @@ msgstr "De bestandsnaam van de/het opgestuurde bestand(en);" msgid "Submission timestamp;" msgstr "Tijdstempel van indiening;" -#: amelie/personal_tab/templates/declaration/declaration_form.html:139 +#: amelie/personal_tab/templates/declaration/declaration_form.html:151 #: templates/basis.html:314 msgid "Submit declaration" msgstr "Declaratie versturen" -#: amelie/personal_tab/templates/declaration/declaration_form.html:173 +#: amelie/personal_tab/templates/declaration/declaration_form.html:185 msgid "You can upload a maximum of" msgstr "Upload maximaal" -#: amelie/personal_tab/templates/declaration/declaration_form.html:173 +#: amelie/personal_tab/templates/declaration/declaration_form.html:185 msgid "files" msgstr "bestanden" -#: amelie/personal_tab/templates/declaration/declaration_form.html:185 +#: amelie/personal_tab/templates/declaration/declaration_form.html:197 msgid "Total file size cannot exceed" msgstr "Totale bestandsgrootte moet kleiner zijn dan" -#: amelie/personal_tab/templates/declaration/declaration_form.html:205 +#: amelie/personal_tab/templates/declaration/declaration_form.html:217 #: amelie/personal_tab/templates/print/form.html:197 msgid "Selected:" msgstr "Geselecteerd:" @@ -12654,27 +14239,75 @@ msgstr "Voor het wijzigen of beëindigen van je machtiging kun je contact opneme msgid "You have not yet signed a direct debit mandate. You can ask the board for one." msgstr "Je hebt nog geen machtiging ingevuld. Je kunt het bestuur om een machtiging vragen." -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:4 +#: amelie/personal_tab/templates/info/payments.html:9 msgid "Direct debits" msgstr "Incasso's" -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:9 -#, python-format +#: amelie/personal_tab/templates/info/payments.html:11 +#, fuzzy +#| msgid "" +#| "\n" +#| " Last %(amount)s executed or planned direct debits.\n" +#| " " msgid "" "\n" -" Last %(amount)s executed or planned direct debits.\n" -" " -msgstr "" +" Your most recently executed or planned direct debit.\n" +" " +msgid_plural "" +"\n" +" Your most recently executed or planned direct debits.\n" +" " +msgstr[0] "" "\n" "Laatste %(amount)s uitgevoerde of geplande incasso's." +msgstr[1] "" +"\n" +"Laatste %(amount)s uitgevoerde of geplande incasso's." + +#: amelie/personal_tab/templates/info/payments.html:42 +msgid "There are no direct debits executed or planned yet." +msgstr "Er zijn nog geen incasso's geweest of gepland." -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:41 +#: amelie/personal_tab/templates/info/payments.html:52 msgid "View all direct debits." msgstr "Alle incasso's bekijken" -#: amelie/personal_tab/templates/info/debt_collection_instructions.html:45 -msgid "There are no direct debits executed or planned yet." -msgstr "Er zijn nog geen incasso's geweest of gepland." +#: amelie/personal_tab/templates/info/payments.html:62 +#, fuzzy +#| msgid "Add amendment" +msgid "Add manual payment" +msgstr "Amendement invoeren" + +#: amelie/personal_tab/templates/info/payments.html:67 +#, fuzzy +#| msgid "" +#| "\n" +#| " Your membership type is %(type)s.\n" +#| " " +msgid "" +"\n" +" Your most recent manual payment.\n" +" " +msgid_plural "" +"\n" +" Your most recent manual payments.\n" +" " +msgstr[0] "" +"\n" +"Je lidmaatschapstype is %(type)s." +msgstr[1] "" +"\n" +"Je lidmaatschapstype is %(type)s." + +#: amelie/personal_tab/templates/info/payments.html:100 +#, fuzzy +#| msgid "There are no contacts yet." +msgid "There are no manual payments yet." +msgstr "Er zijn nog geen contacten." + +#: amelie/personal_tab/templates/info/payments.html:110 +msgid "View all manual payments." +msgstr "" #: amelie/personal_tab/templates/info/rfid_cards.html:13 msgid "Type of card" @@ -12692,97 +14325,243 @@ msgstr "Voor September 2019" msgid "Deactivate" msgstr "Deactiveren" -#: amelie/personal_tab/templates/info/transaction_information.html:29 -#: amelie/personal_tab/templates/statistics/statistics.html:375 -#: amelie/personal_tab/templates/statistics/statistics.html:410 +#: amelie/personal_tab/templates/info/transaction_information.html:26 +#: amelie/personal_tab/templates/statistics/statistics.html:373 +#: amelie/personal_tab/templates/statistics/statistics.html:408 msgid "Discount offer" msgstr "Kortingsactie" -#: amelie/personal_tab/templates/info/transaction_information.html:33 +#: amelie/personal_tab/templates/info/transaction_information.html:30 msgid "Discount amount" msgstr "Kortingsbedrag" -#: amelie/personal_tab/templates/lists/activity_transactions.html:9 -msgid "Added" -msgstr "Toegevoegd" - -#: amelie/personal_tab/templates/lists/activity_transactions.html:41 +#: amelie/personal_tab/templates/info/transaction_information.html:71 +#: amelie/personal_tab/templates/lists/activity_transactions.html:49 #: amelie/personal_tab/templates/statistics/statistics.html:205 -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:23 msgid "Deleted event" msgstr "Verwijderd event" -#: amelie/personal_tab/templates/lists/activity_transactions.html:64 +#: amelie/personal_tab/templates/info/transaction_information.html:83 +msgid "Transaction number Alexia" +msgstr "Transactienummer Alexia" + +#: amelie/personal_tab/templates/info/transaction_information.html:108 +msgid "Reversed collection" +msgstr "Teruggeboekte incasso" + +#: amelie/personal_tab/templates/info/transaction_information.html:122 +#, fuzzy +#| msgid "Original" +msgid "Originated in" +msgstr "Origineel" + +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:3 +#, fuzzy +#| msgid "Print membership forms (opens in new tab)" +msgid "Show settlement details (opens in new tab)" +msgstr "Lidmaatschapsformulieren printen (opent in nieuwe tab)" + +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:9 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Add credit for %(person)s.\n" +#| " " +msgid "" +"\n" +" Direct debit on %(date)s\n" +" " +msgstr "" +"\n" +"Tentamenkoekentegoed toevoegen voor %(person)s" + +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:17 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Enroll person for %(name)s\n" +#| " " +msgid "" +"\n" +" Reversal on %(date)s\n" +" " +msgstr "" +"\n" +"Persoon inschrijven voor %(name)s" + +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:24 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Enroll person for %(name)s\n" +#| " " +msgid "" +"\n" +" Manual payment on %(date)s\n" +" " +msgstr "" +"\n" +"Persoon inschrijven voor %(name)s" + +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:35 +#, fuzzy +#| msgid "No payment" +msgid "UNKNOWN payment!" +msgstr "Geen betaling" + +#: amelie/personal_tab/templates/info/transaction_settlement_info.html:40 +msgid "Unpaid!" +msgstr "" + +#: amelie/personal_tab/templates/lists/activity_transactions.html:10 +msgid "Added" +msgstr "Toegevoegd" + +#: amelie/personal_tab/templates/lists/activity_transactions.html:72 msgid "No activity transactions." msgstr "Geen activiteittransacties." -#: amelie/personal_tab/templates/lists/alexia_transactions.html:9 -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:9 -#: amelie/personal_tab/templates/lists/custom_transactions.html:9 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:9 -#: amelie/personal_tab/templates/lists/transactions.html:9 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:10 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:10 +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:10 +#: amelie/personal_tab/templates/lists/custom_transactions.html:10 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:10 +#: amelie/personal_tab/templates/lists/payment_transactions.html:11 +#: amelie/personal_tab/templates/lists/transactions.html:10 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:10 #: amelie/personal_tab/templates/statistics/statistics.html:185 #: amelie/personal_tab/templates/statistics/statistics.html:320 msgid "Time" msgstr "Tijd" -#: amelie/personal_tab/templates/lists/alexia_transactions.html:50 +#: amelie/personal_tab/templates/lists/alexia_transactions.html:58 msgid "No Alexia transactions." msgstr "Geen Alexia-transacties." -#: amelie/personal_tab/templates/lists/authorization_select.html:55 +#: amelie/personal_tab/templates/lists/authorization_select.html:60 msgid "No mandates have been found." msgstr "Er zijn geen machtigingen gevonden." -#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:57 +#: amelie/personal_tab/templates/lists/contribution_transactions.html:58 +#, fuzzy +#| msgid "Contribution transactions" +msgid "No contribution transactions." +msgstr "Contributietransacties" + +#: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:65 msgid "No transactions on personal tab." msgstr "Geen streeplijsttransacties." -#: amelie/personal_tab/templates/lists/custom_transactions.html:50 +#: amelie/personal_tab/templates/lists/custom_transactions.html:58 msgid "No other transactions." msgstr "Geen overige transacties." -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:8 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:8 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:9 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:9 msgid "Collect" msgstr "Incasseren" -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:33 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:31 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:43 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:41 msgctxt "amendment abbr" msgid "A " msgstr "A " -#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:60 -#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:57 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_contribution.html:70 +#: amelie/personal_tab/templates/lists/debt_collection_proposal_cookie_corner.html:69 msgid "There are no collections in this category." msgstr "Er zijn geen incasso's in deze categorie." -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:10 -#: amelie/personal_tab/templates/lists/transactions_totals.html:10 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:11 +#: amelie/personal_tab/templates/lists/transactions_totals.html:11 msgid "Month" msgstr "Maand" -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:14 -#: amelie/personal_tab/templates/lists/transactions_totals.html:14 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:15 +#: amelie/personal_tab/templates/lists/transactions_totals.html:15 msgid "Day" msgstr "Dag" -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:22 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:23 msgid "Credit" msgstr "Tegoed" -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:23 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:24 msgid "Usage" msgstr "Verbruik" -#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:59 -#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:46 -#: amelie/personal_tab/templates/lists/transactions.html:53 -#: amelie/personal_tab/templates/lists/transactions_totals.html:67 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_totals.html:60 +#: amelie/personal_tab/templates/lists/exam_cookie_credit_transactions.html:54 +#: amelie/personal_tab/templates/lists/transactions.html:33 +#: amelie/personal_tab/templates/lists/transactions_categorized.html:48 +#: amelie/personal_tab/templates/lists/transactions_totals.html:79 msgid "No transactions" msgstr "Geen transacties" +#: amelie/personal_tab/templates/lists/payment_transactions.html:44 +#, fuzzy +#| msgid "Direct debits" +msgid "Direct debit" +msgstr "Incasso's" + +#: amelie/personal_tab/templates/lists/payment_transactions.html:46 +#, fuzzy +#| msgid "Reversal of direct withdrawal {date}" +msgid "Reversal of direct debit" +msgstr "Terugboeking incasso {date}" + +#: amelie/personal_tab/templates/lists/payment_transactions.html:78 +#, fuzzy +#| msgid "No payment" +msgid "No payments." +msgstr "Geen betaling" + +#: amelie/personal_tab/templates/lists/transactions_totals.html:51 +msgid "This date is in the future" +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:4 +msgid "[Inter-Actief] Reminder of open balance on your personal tab" +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:8 +msgid "This e-mail serves as a reminder that you have an open balance on your personal tab that needs to be paid." +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:10 +msgid "Because you do not have an active, signed mandate for consumptions, we cannot collect this money from your bank automatically, so you will need to pay manually. Please visit the association room or contact the board to pay the balance by cash, bank transfer or PIN." +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:12 +#, python-format +msgid "Your open balance is %(costs)s euro. This consists of %(num_unpaid)s transactions." +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:14 +#, python-format +msgid "To view more details about the balance and the unpaid transactions, you can view your unpaid transaction overview." +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:15 +#, python-format +msgid "To view more details about the balance and the unpaid transactions, you can view your unpaid transaction overview at %(link)s" +msgstr "" + +#: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:19 +#, fuzzy +#| msgid "" +#| "Kind regards,\n" +#| "\n" +#| "The board of Inter-Actief" +msgid "" +"Kind regards,\n" +"\n" +"The treasurer of Inter-Actief" +msgstr "" +"Met vriendelijke groet,\n" +"\n" +"Het bestuur van Inter-Actief" + #: amelie/personal_tab/templates/pos/base.html:6 msgid "Inter-/Actief/ Cookie Corner Point of Sale" msgstr "Inter-/Actief/ Cookie Corner Point of Sale" @@ -12799,7 +14578,7 @@ msgstr "Of tik hier om in te loggen" #: amelie/personal_tab/templates/pos/register_external_card.html:19 #: amelie/personal_tab/templates/pos/scan_external_card.html:13 #: amelie/personal_tab/templates/pos/shop.html:9 -#: amelie/personal_tab/templates/pos/show_qr.html:13 +#: amelie/personal_tab/templates/pos/show_qr.html:19 #: amelie/personal_tab/templates/pos/success.html:19 #: amelie/personal_tab/templates/pos/verify.html:8 msgid "THIS COOKIE CORNER IS RUNNING IN BETA MODE. PLEASE DO NOT BUY ANYTHING." @@ -12858,7 +14637,8 @@ msgstr "Enschede" #: amelie/personal_tab/templates/pos/register_external_card.html:16 #: amelie/personal_tab/templates/pos/scan_external_card.html:10 -#: amelie/personal_tab/templates/pos/show_qr.html:10 +#: amelie/personal_tab/templates/pos/show_qr.html:11 +#: amelie/personal_tab/templates/pos/show_qr.html:15 #: amelie/personal_tab/templates/pos/success.html:16 msgid "Return to home" msgstr "Terug naar hoofdpagina" @@ -12935,30 +14715,29 @@ msgstr "Winkelmandje" msgid "And {amount} more articles..." msgstr "En nog {amount} andere artikelen..." -#: amelie/personal_tab/templates/pos/show_qr.html:19 -#: amelie/personal_tab/templates/register/register_card_index.html:28 -#: templates/basis.html:324 templates/login.html:4 templates/login.html:47 +#: amelie/personal_tab/templates/pos/show_qr.html:25 templates/basis.html:324 +#: templates/login.html:4 templates/login.html:47 msgid "Log in" msgstr "Inloggen" -#: amelie/personal_tab/templates/pos/show_qr.html:21 +#: amelie/personal_tab/templates/pos/show_qr.html:27 msgid "Register new RFID card" msgstr "Registreer nieuwe RFID-kaart" -#: amelie/personal_tab/templates/pos/show_qr.html:23 +#: amelie/personal_tab/templates/pos/show_qr.html:29 #: amelie/personal_tab/templates/pos/verify.html:13 msgid "Error" msgstr "Fout" -#: amelie/personal_tab/templates/pos/show_qr.html:28 +#: amelie/personal_tab/templates/pos/show_qr.html:34 msgid "Scan the QR code below to log in." msgstr "Scan de QR code hieronder om in te loggen." -#: amelie/personal_tab/templates/pos/show_qr.html:33 +#: amelie/personal_tab/templates/pos/show_qr.html:39 msgid "Scan the QR code below to register your new card." msgstr "Scan de QR code hieronder om je kaart te registreren." -#: amelie/personal_tab/templates/pos/show_qr.html:38 +#: amelie/personal_tab/templates/pos/show_qr.html:44 msgid "Something went wrong. Please go back to the main page." msgstr "Er ging iets fout. Ga alsjebleift terug naar de hoofdpagina." @@ -12967,7 +14746,7 @@ msgid "Success!" msgstr "Succes!" #: amelie/personal_tab/templates/pos/success.html:33 -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:30 +#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:26 msgid "Product" msgstr "Product" @@ -12975,10 +14754,6 @@ msgstr "Product" msgid "Price per piece" msgstr "Stukprijs" -#: amelie/personal_tab/templates/pos/success.html:39 -msgid "Subtotal" -msgstr "Subtotaal" - #: amelie/personal_tab/templates/pos/success.html:58 msgid "Total:" msgstr "Totaal:" @@ -13179,60 +14954,60 @@ msgstr "" " %(time)s geleden\n" " " -#: amelie/personal_tab/templates/print/log_list.html:58 +#: amelie/personal_tab/templates/print/log_list.html:63 #, python-format msgid "Job %(job_id)s" msgstr "Opdracht %(job_id)s" -#: amelie/personal_tab/templates/print/log_list.html:61 +#: amelie/personal_tab/templates/print/log_list.html:66 msgid "No job id!" msgstr "Geen opdracht-id!" -#: amelie/personal_tab/templates/print/log_list.html:77 +#: amelie/personal_tab/templates/print/log_list.html:82 #: amelie/personal_tab/templates/print/refund.html:50 msgid "View transaction" msgstr "Transactie bekijken" -#: amelie/personal_tab/templates/print/log_list.html:81 +#: amelie/personal_tab/templates/print/log_list.html:86 #: amelie/personal_tab/templates/print/refund.html:54 #, python-format msgid "(€ %(price)s)" msgstr "(€ %(price)s)" -#: amelie/personal_tab/templates/print/log_list.html:84 +#: amelie/personal_tab/templates/print/log_list.html:89 msgid "Refund this print job" msgstr "Betaal deze printopdracht terug" -#: amelie/personal_tab/templates/print/log_list.html:86 +#: amelie/personal_tab/templates/print/log_list.html:91 msgid "Refund" msgstr "Terugbetalen" -#: amelie/personal_tab/templates/print/log_list.html:102 +#: amelie/personal_tab/templates/print/log_list.html:107 #: amelie/personal_tab/templates/print/refund.html:74 msgid "Chrome" msgstr "Chrome" -#: amelie/personal_tab/templates/print/log_list.html:104 +#: amelie/personal_tab/templates/print/log_list.html:109 #: amelie/personal_tab/templates/print/refund.html:76 msgid "Firefox" msgstr "Firefox" -#: amelie/personal_tab/templates/print/log_list.html:106 +#: amelie/personal_tab/templates/print/log_list.html:111 #: amelie/personal_tab/templates/print/refund.html:78 msgid "Safari" msgstr "Safari" -#: amelie/personal_tab/templates/print/log_list.html:108 +#: amelie/personal_tab/templates/print/log_list.html:113 #: amelie/personal_tab/templates/print/refund.html:80 msgid "Edge" msgstr "Edge" -#: amelie/personal_tab/templates/print/log_list.html:110 +#: amelie/personal_tab/templates/print/log_list.html:115 #: amelie/personal_tab/templates/print/refund.html:82 msgid "Browser" msgstr "Browser" -#: amelie/personal_tab/templates/print/log_list.html:124 +#: amelie/personal_tab/templates/print/log_list.html:129 msgid "No print log entries found." msgstr "Geen printlogboekvermeldingen gevonden." @@ -13263,70 +15038,54 @@ msgstr "" msgid "Document Name" msgstr "Documentnaam" -#: amelie/personal_tab/templates/register/register_card_error.html:4 -#: amelie/personal_tab/templates/register/register_card_error.html:8 -msgid "Register an RFID card - Error" -msgstr "Registreer een RFID-kaart - Error" - -#: amelie/personal_tab/templates/register/register_card_error.html:10 -msgid "Something went wrong while registering your RFID card" -msgstr "Er ging iets fout bij het registreren van je RFID-kaart" - #: amelie/personal_tab/templates/register/register_card_index.html:4 -#: amelie/personal_tab/templates/register/register_card_index.html:8 -#: amelie/personal_tab/templates/register/register_card_scan.html:4 -#: amelie/personal_tab/templates/register/register_card_scan.html:44 +#: amelie/personal_tab/templates/register/register_card_index.html:51 msgid "Register an RFID card" msgstr "Registreer een RFID-kaart" -#: amelie/personal_tab/templates/register/register_card_index.html:10 -msgid "Log in to register a card" -msgstr "Log in om een kaart te registreren" - -#: amelie/personal_tab/templates/register/register_card_index.html:12 -msgid "You can log on with your student account (for example: s0123456) or your Inter-Actief account." -msgstr "Je kunt inloggen met je studentaccount (bijvoorbeeld s0123456) of je Inter-Actief-account." - -#: amelie/personal_tab/templates/register/register_card_index.html:23 -msgid "Password" -msgstr "Wachtwoord" - -#: amelie/personal_tab/templates/register/register_card_scan.html:11 +#: amelie/personal_tab/templates/register/register_card_index.html:11 msgid "Please scan your card now..." msgstr "Scan een kaart om te registreren..." -#: amelie/personal_tab/templates/register/register_card_scan.html:15 +#: amelie/personal_tab/templates/register/register_card_index.html:15 msgid "Connection with NFC-reader failed." msgstr "Verbinden met NFC-reader mislukt." -#: amelie/personal_tab/templates/register/register_card_scan.html:26 +#: amelie/personal_tab/templates/register/register_card_index.html:25 msgid "Scanned card, please wait..." msgstr "Kaart gescand, een moment geduld aub..." -#: amelie/personal_tab/templates/register/register_card_scan.html:30 +#: amelie/personal_tab/templates/register/register_card_index.html:29 msgid "Card type is unknown. Please use normal RFID cards like your college card or public transport card." msgstr "Kaarttype is onbekend. Probeer een normale RFID kaart te scannen zoals je collegekaart of OV-chipkaart." -#: amelie/personal_tab/templates/register/register_card_scan.html:32 +#: amelie/personal_tab/templates/register/register_card_index.html:31 msgid "Error, could not scan card." msgstr "Error, kon kaart niet scannen." -#: amelie/personal_tab/templates/register/register_card_scan.html:46 +#: amelie/personal_tab/templates/register/register_card_index.html:41 +#, fuzzy +#| msgid "Scanned card, please wait..." +msgid "Received card, please wait..." +msgstr "Kaart gescand, een moment geduld aub..." + +#: amelie/personal_tab/templates/register/register_card_index.html:53 msgid "Connecting to card reader..." msgstr "Verbinden met kaartlezer..." -#: amelie/personal_tab/templates/register/register_card_success.html:4 -#: amelie/personal_tab/templates/register/register_card_success.html:13 -msgid "Register an RFID card - Success" -msgstr "Registreer een RFID-kaart - Succes" - -#: amelie/personal_tab/templates/register/register_card_success.html:17 -msgid "You will be brought back to the registration screen in 5 seconds..." -msgstr "Je wordt over 5 seconden teruggebracht naar het registratiescherm..." - -#: amelie/personal_tab/templates/register/register_card_success.html:19 -msgid "Go back now" -msgstr "Ga nu terug" +#: amelie/personal_tab/templates/register/register_card_index.html:60 +#, fuzzy +#| msgid "" +#| "\n" +#| " Are you sure you want to remove the alias %(object)s?\n" +#| " " +msgid "" +"\n" +" By registering your card, you can use it to make purchases in the Cookie Corner or at our drinks.\n" +" " +msgstr "" +"\n" +"Weet je zeker dat je alias %(object)s wilt verwijderen?" #: amelie/personal_tab/templates/statistics/statistics.html:23 msgid "End date (till)" @@ -13340,7 +15099,7 @@ msgstr "Inschrijvingen" msgid "Drink" msgstr "Borrel" -#: amelie/personal_tab/templates/statistics/statistics.html:455 +#: amelie/personal_tab/templates/statistics/statistics.html:453 msgid "There are statistics to be shown." msgstr "Er zijn statistieken om te laten zien." @@ -13358,60 +15117,48 @@ msgstr "Er zijn statistieken om te laten zien." msgid "Transaction information" msgstr "Transactiegegevens" -#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:46 +#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:26 msgid "Question" msgstr "Vraag" -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:14 -msgid "Transaction number Alexia" -msgstr "Transactienummer Alexia" +#: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:27 +#, fuzzy +#| msgid "Respons" +msgid "Response" +msgstr "Antwoord" -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:22 +#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:18 msgid "Purchases" msgstr "Aankopen" -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:44 +#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:40 msgid "No purchases." msgstr "Geen aankopen." -#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:51 +#: amelie/personal_tab/templates/transactions/alexia_transaction_detail.html:47 msgid "It was not possible to retrieve the transaction details." msgstr "Het was niet mogelijk de transactiedetails op te halen." -#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:28 +#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:16 #: amelie/personal_tab/templates/transactions/transaction_detail.html:16 msgid "Edit transaction" msgstr "Transactie wijzigen" -#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:29 +#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:17 #: amelie/personal_tab/templates/transactions/transaction_detail.html:17 msgid "Delete transaction" msgstr "Verwijder transactie" -#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:31 +#: amelie/personal_tab/templates/transactions/cookie_corner_transaction_detail.html:19 #: amelie/personal_tab/templates/transactions/transaction_detail.html:19 msgid "This transaction is not editable as it has already been debited." msgstr "De transactie is niet aan te passen omdat deze al geïncasseerd is." -#: amelie/personal_tab/templates/transactions/reversal_transaction_detail.html:14 -msgid "Reversed collection" -msgstr "Teruggeboekte incasso" - #: amelie/personal_tab/templates/transactions/transaction_delete.html:8 #, python-format msgid "Delete transaction '%(object)s'" msgstr "Verwijder transactie '%(object)s'" -#: amelie/personal_tab/templates/transactions/transaction_delete.html:11 -#, python-format -msgid "" -"\n" -" Are you sure you want to delete '%(object)s' for %(person)s?\n" -" " -msgstr "" -"\n" -"Weet je zeker dat je '%(object)s' van %(person)s wilt verwijderen?" - #: amelie/personal_tab/templates/transactions/transaction_form.html:5 #: amelie/personal_tab/templates/transactions/transaction_form.html:13 msgid "Add transaction" @@ -13422,14 +15169,6 @@ msgstr "Transactie toevoegen" msgid "Edit transaction '%(object)s'" msgstr "Wijzig transactie '%(object)s'" -#: amelie/personal_tab/templates/transactions/transaction_form.html:20 -msgid "You are now adding a transaction to a personal tab" -msgstr "Je voegt nu een streeplijsttransactie toe" - -#: amelie/personal_tab/templates/transactions/transaction_form.html:22 -msgid "You are now adding a transaction in the category \"Other\"" -msgstr "Je voegt nu een overige transactie toe" - #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:4 #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:9 msgid "Forgive Unpaid Memberships" @@ -13440,14 +15179,10 @@ msgid "You are about to forgive the membership fee of the following persons:" msgstr "Je staat op het punt de contributie kwijt te schelden voor de volgende personen:" #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:16 -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:23 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:32 msgid "Current Member?" msgstr "Huidig lid?" -#: amelie/personal_tab/templates/unpaid_memberships/forgive.html:48 -msgid "Confirm" -msgstr "Bevestigen" - #: amelie/personal_tab/templates/unpaid_memberships/overview.html:4 #: amelie/personal_tab/templates/unpaid_memberships/overview.html:8 #: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:4 @@ -13463,14 +15198,30 @@ msgstr "Klik op een jaar om te zien wiens lidmaatschappen nog onbetaald zijn." msgid "Price Total" msgstr "Totaalprijs" -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:25 -msgid "Debit Refused?" -msgstr "Incasso geweigerd?" +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:12 +#, fuzzy +#| msgid "You are about to forgive the membership fee of the following persons:" +msgid "Use these buttons to send a mailing or forgive the membership fee of the selected members." +msgstr "Je staat op het punt de contributie kwijt te schelden voor de volgende personen:" -#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:73 +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:16 msgid "Forgive membership fee" msgstr "Contributie kwijtschelden" +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:34 +msgid "Debit Refused?" +msgstr "Incasso geweigerd?" + +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:94 +#, fuzzy +#| msgid "Unpaid memberships" +msgid "No unpaid memberships" +msgstr "Onbetaalde lidmaatschappen" + +#: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:96 +msgid "There are no unpaid memberships in this year! Woah! 🎉🎉🎉" +msgstr "" + #: amelie/personal_tab/templates/wrapped/wrapped.html:4 #: amelie/personal_tab/templates/wrapped/wrapped.html:10 msgid " - Global statistics" @@ -13652,114 +15403,188 @@ msgstr "" msgid "Look back on a different year?" msgstr "Een ander jaar terugkijken?" -#: amelie/personal_tab/transactions.py:58 +#: amelie/personal_tab/transactions.py:74 +#, fuzzy, python-brace-format +#| msgid "Edit enrollment options for %(activity)s" +msgid "Settlement of previous costs for {activity}" +msgstr "Inschrijfopties voor %(activity)s wijzigen" + +#: amelie/personal_tab/transactions.py:105 #, python-brace-format msgid "Edited enrollment for {activity} (addition of updated costs)" msgstr "Inchrijving voor {activity} aangepast (toevoegen bijgewerkte kosten)" -#: amelie/personal_tab/transactions.py:60 +#: amelie/personal_tab/transactions.py:109 #, python-brace-format msgid "Enrolled for {activity}" msgstr "Inschrijven {activity}" -#: amelie/personal_tab/transactions.py:70 +#: amelie/personal_tab/transactions.py:121 #, python-brace-format msgid "Edited enrollment for {activity} (reversal of old costs)" msgstr "Inchrijving voor {activity} aangepast (terugboeken oude kosten)" -#: amelie/personal_tab/transactions.py:72 +#: amelie/personal_tab/transactions.py:125 #, python-brace-format msgid "Unenrolled for {activity}" msgstr "Uitschrijven {activity}" -#: amelie/personal_tab/transactions.py:115 -#: amelie/personal_tab/transactions.py:215 +#: amelie/personal_tab/transactions.py:168 +#: amelie/personal_tab/transactions.py:269 #, python-brace-format msgid "Sale {category}::{article}" msgstr "Verkoop {category}::{article}" -#: amelie/personal_tab/views.py:543 amelie/personal_tab/views.py:1016 -#: amelie/personal_tab/views.py:1047 amelie/personal_tab/views.py:1157 +#: amelie/personal_tab/views.py:560 amelie/personal_tab/views.py:1430 +#: amelie/personal_tab/views.py:1461 amelie/personal_tab/views.py:1571 msgid "Invalid date`" msgstr "Ongeldige datum" -#: amelie/personal_tab/views.py:591 amelie/personal_tab/views.py:599 -#: amelie/personal_tab/views.py:629 amelie/personal_tab/views.py:638 +#: amelie/personal_tab/views.py:610 amelie/personal_tab/views.py:618 +#: amelie/personal_tab/views.py:674 amelie/personal_tab/views.py:683 msgid "No members were selected." msgstr "Er zijn geen leden geselecteerd." -#: amelie/personal_tab/views.py:615 -msgid "The membership fees have been forgiven." +#: amelie/personal_tab/views.py:643 +#, fuzzy, python-brace-format +#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +msgid "Forgiven membership costs for {membership_type} ({begin_year}/{end_year})" +msgstr "Contributie {membership_type} ({begin_year}/{end_year})" + +#: amelie/personal_tab/views.py:660 +#, fuzzy, python-brace-format +#| msgid "The membership fees have been forgiven." +msgid "The membership fees for {amount} memberships have been forgiven." msgstr "De contributie is kwijtgescholden." -#: amelie/personal_tab/views.py:721 amelie/personal_tab/views.py:1912 +#: amelie/personal_tab/views.py:766 amelie/personal_tab/views.py:2323 msgid "The e-mails are now being sent one by one. This happens in a background process, so it may take a while." msgstr "De e-mails worden nu een voor een verstuurd. Dit gebeurt in een achtergrondproces en kan even duren." -#: amelie/personal_tab/views.py:830 +#: amelie/personal_tab/views.py:874 msgid "Transaction not editable." msgstr "Transactie niet te wijzigen" -#: amelie/personal_tab/views.py:855 +#: amelie/personal_tab/views.py:899 #, python-brace-format msgid "The transaction '{transaction}' has been successfully deleted." msgstr "De transactie '{transaction}' is met success verwijderd." -#: amelie/personal_tab/views.py:862 +#: amelie/personal_tab/views.py:906 msgid "Transaction not deletable." msgstr "Transactie niet te verwijderen" -#: amelie/personal_tab/views.py:950 amelie/personal_tab/views.py:1114 +#: amelie/personal_tab/views.py:992 amelie/personal_tab/views.py:1528 msgid "Invalid date" msgstr "Ongeldige datum" -#: amelie/personal_tab/views.py:1111 +#: amelie/personal_tab/views.py:1203 +#, fuzzy +#| msgid "Personal tab statistics" +msgid "Personal tab transactions in this settlement" +msgstr "Streeplijststatistieken" + +#: amelie/personal_tab/views.py:1211 +#, fuzzy +#| msgid "No other transactions." +msgid "Payment for the transactions above" +msgstr "Geen overige transacties." + +#: amelie/personal_tab/views.py:1219 +#, fuzzy +#| msgid "Transactions from personal tab:" +msgid "Residual balance debited from personal tab" +msgstr "Transacties uit koekenkast:" + +#: amelie/personal_tab/views.py:1221 +#, fuzzy +#| msgid "Add transaction to personal tab" +msgid "Residual balance credited to personal tab" +msgstr "Streeplijsttransactie toevoegen" + +#: amelie/personal_tab/views.py:1281 +#, python-brace-format +msgid "Manual payment '{payment}' of '{person}' deleted." +msgstr "" + +#: amelie/personal_tab/views.py:1363 +#, python-brace-format +msgid "Personal tab settlement on {date} for {name}" +msgstr "" + +#: amelie/personal_tab/views.py:1368 +#, fuzzy, python-brace-format +#| msgid "Mandate form #{id} for {name}" +msgid "Refund on {date} for {name}" +msgstr "Machtigingsformulier #{id} voor {name}" + +#: amelie/personal_tab/views.py:1373 +#, fuzzy, python-brace-format +#| msgid "Mandate form #{id} for {name}" +msgid "Manual payment on {date} for {name}" +msgstr "Machtigingsformulier #{id} voor {name}" + +#: amelie/personal_tab/views.py:1379 +#, python-brace-format +msgid "Debit from personal tab balance on {date} for {name}" +msgstr "" + +#: amelie/personal_tab/views.py:1384 +#, python-brace-format +msgid "Credit to personal tab balance on {date} for {name}" +msgstr "" + +#: amelie/personal_tab/views.py:1404 +msgid "No settlement was created because no transactions were selected and no amount was paid." +msgstr "" + +#: amelie/personal_tab/views.py:1525 msgid "Invalid statistics list" msgstr "Ongeldige statistiekenlijst" -#: amelie/personal_tab/views.py:1346 +#: amelie/personal_tab/views.py:1757 msgid "End inactive mandates." msgstr "Inactieve machtigingen beëindigd." -#: amelie/personal_tab/views.py:1401 +#: amelie/personal_tab/views.py:1812 msgid "This mandate is terminated, so an amendment cannot be made" msgstr "Deze machtiging is beëindigd, dus er kan geen amendenemt worden aangemaakt" -#: amelie/personal_tab/views.py:1404 +#: amelie/personal_tab/views.py:1815 msgid "This mandate is not signed, so an amendment cannot be made" msgstr "Deze machtiging is niet getekend, dus er kan geen amendenemt worden aangemaakt" -#: amelie/personal_tab/views.py:1407 +#: amelie/personal_tab/views.py:1818 msgid "This mandate already has an ongoing amendment" msgstr "Deze machtiging heeft al een amendement in behandeling" -#: amelie/personal_tab/views.py:1434 amelie/personal_tab/views.py:1470 +#: amelie/personal_tab/views.py:1845 amelie/personal_tab/views.py:1881 msgid "This amendment does not belong to this authorization" msgstr "Dit amendement hoort niet bij deze machtiging" -#: amelie/personal_tab/views.py:1437 amelie/personal_tab/views.py:1473 +#: amelie/personal_tab/views.py:1848 amelie/personal_tab/views.py:1884 msgid "This amendment is not editable" msgstr "Dit amendement kan niet worden aangepast" -#: amelie/personal_tab/views.py:1673 +#: amelie/personal_tab/views.py:2084 msgid "Reversal already exists" msgstr "Terugboeking bestaat al" -#: amelie/personal_tab/views.py:1699 amelie/personal_tab/views.py:1731 +#: amelie/personal_tab/views.py:2110 amelie/personal_tab/views.py:2142 msgid "Reversal does not exist" msgstr "Terugboeking bestaat niet" -#: amelie/personal_tab/views.py:1888 amelie/personal_tab/views.py:1982 +#: amelie/personal_tab/views.py:2299 amelie/personal_tab/views.py:2393 #: amelie/room_duty/templates/room_duty/pool/persons_change.html:52 #: amelie/room_duty/templates/room_duty/table/change_persons.html:52 msgid "Found nobody." msgstr "Geen personen gevonden." -#: amelie/personal_tab/views.py:2319 +#: amelie/personal_tab/views.py:2730 msgid "Declaration was submitted successfully." msgstr "Declaratie is succesvol verstuurd." -#: amelie/personal_tab/views.py:2324 +#: amelie/personal_tab/views.py:2735 #, python-brace-format msgid "Error while submitting declaration: {ex}" msgstr "Fout bij het versturen van de declaratie: {ex}" @@ -14248,27 +16073,27 @@ msgstr "Dutch" msgid "Engels" msgstr "English" -#: amelie/settings/generic.py:121 +#: amelie/settings/generic.py:128 msgid "Dutch" msgstr "Nederlands" -#: amelie/settings/generic.py:122 +#: amelie/settings/generic.py:129 msgid "English" msgstr "Engels" -#: amelie/settings/generic.py:854 +#: amelie/settings/generic.py:870 msgid "Access to your name, date of birth, student number, mandate status and committee status." msgstr "Toegang tot je naam, geboortedatum, studentnummer, machtiging- en commissiestatus" -#: amelie/settings/generic.py:855 +#: amelie/settings/generic.py:871 msgid "Access to enrollments for activities and (un)enrolling you for activities." msgstr "Toegang tot inschrijvingen voor activiteiten en het in- en uitschrijven voor activiteiten" -#: amelie/settings/generic.py:856 +#: amelie/settings/generic.py:872 msgid "Access to transactions, direct debit transactions, mandates and RFID-cards." msgstr "Toegang tot transacties, incasso's, machtigingen en RFID-kaarten" -#: amelie/settings/generic.py:857 +#: amelie/settings/generic.py:873 msgid "Access to complaints and sending or supporting complaints in your name." msgstr "Toegang tot onderwijsklachten en het indienen of steunen van onderwijsklachten" @@ -14448,7 +16273,7 @@ msgstr "[START] {}" msgid "[END] {}" msgstr "[EIND] {}" -#: amelie/tools/decorators.py:42 +#: amelie/tools/decorators.py:42 amelie/tools/mixins.py:93 msgid "AJAX request required" msgstr "AJAX-request vereist" @@ -14456,26 +16281,26 @@ msgstr "AJAX-request vereist" msgid "Access for board members only." msgstr "Uitsluitend toegankelijk voor het bestuur." -#: amelie/tools/decorators.py:54 amelie/tools/mixins.py:110 +#: amelie/tools/decorators.py:54 amelie/tools/mixins.py:116 msgid "Access for the Educational Committee only." msgstr "Uitsluitend toegankelijk voor de onderwijscommissie." #: amelie/tools/decorators.py:60 amelie/tools/decorators.py:66 -#: amelie/tools/mixins.py:118 +#: amelie/tools/mixins.py:124 msgid "Only for members with an active membership." msgstr "Alleen voor leden met een actief lidmaatschap." -#: amelie/tools/decorators.py:72 amelie/tools/mixins.py:134 +#: amelie/tools/decorators.py:72 amelie/tools/mixins.py:140 msgid "Access for active members only." msgstr "Uitsluitend toegankelijk voor actieve leden." -#: amelie/tools/decorators.py:77 amelie/tools/mixins.py:153 -#: amelie/tools/mixins.py:161 +#: amelie/tools/decorators.py:77 amelie/tools/mixins.py:159 +#: amelie/tools/mixins.py:167 msgid "Only accessible by superusers." msgstr "Uitsluitend toegankelijk voor superusers." -#: amelie/tools/decorators.py:88 amelie/tools/mixins.py:172 -#: amelie/tools/mixins.py:188 amelie/tools/mixins.py:204 +#: amelie/tools/decorators.py:88 amelie/tools/mixins.py:178 +#: amelie/tools/mixins.py:194 amelie/tools/mixins.py:210 msgid "Access for members of the committee only." msgstr "Uitsluitend toegankelijk voor commissieleden." @@ -14644,31 +16469,31 @@ msgstr "De URL naar de gegenereerde afbeelding voor deze CAPTCHA" msgid "The response to the CAPTCHA challenge" msgstr "Het antwoord op de CAPTCHA challenge" -#: amelie/tools/mixins.py:94 +#: amelie/tools/mixins.py:100 msgid "Access for (former) members only." msgstr "Uitsluitend toegankelijk voor (oud-)leden." -#: amelie/tools/mixins.py:102 +#: amelie/tools/mixins.py:108 msgid "Access for boardmembers only." msgstr "Uitsluitend toegankelijk voor het bestuur." -#: amelie/tools/mixins.py:126 +#: amelie/tools/mixins.py:132 msgid "Only for members with a signed and active personal tab authorization." msgstr "Alleen voor mensen met een getekende en geactiveerde streeplijstmachtiging." -#: amelie/tools/mixins.py:142 +#: amelie/tools/mixins.py:148 msgid "Only for active members, or members with a signed and active personal tab authorization." msgstr "Alleen voor actieve leden, of voor mensen met een getekende en geactiveerde streeplijstmachtiging." -#: amelie/tools/mixins.py:232 +#: amelie/tools/mixins.py:238 msgid "Access for do-group parents only." msgstr "Uitsluitend toegankelijk voor doegroepouders." -#: amelie/tools/mixins.py:254 +#: amelie/tools/mixins.py:260 msgid "Access for allowlisted IP address only." msgstr "Uitsluitend toegankelijk voor bekende IP-adressen." -#: amelie/tools/mixins.py:278 +#: amelie/tools/mixins.py:284 msgid "Access for the cookie-corner computer only." msgstr "Uitsluitend toegankelijk voor de streeplijstcomputer." @@ -15181,12 +17006,12 @@ msgstr "Het is niet gelukt om een verbinding met Streaming.IA te maken! Neem con msgid "Could not connect to Video.IA! Please contact the WWW committee if this problem persists." msgstr "Het is niet gelukt om een verbinding met Video.IA te maken! Neem contact op met de WWW commissie als dit probleem blijft aanhouden." -#: amelie/views.py:313 +#: amelie/views.py:312 #, python-format msgid "CSRF-check failed. Reason: %s" msgstr "CSRF-controle mislukt. Reden: %s" -#: amelie/views.py:315 +#: amelie/views.py:314 msgid "CSRF-check failed." msgstr "CSRF-controle mislukt." @@ -16192,17 +18017,35 @@ msgstr "" "\n" "Je lidmaatschapstype is %(type)s." -#: templates/profile_overview.html:63 -#, python-format +#: templates/profile_overview.html:62 +#, fuzzy +#| msgid "" +#| "\n" +#| " Your membership type is %(type)s.\n" +#| " " msgid "" "\n" -" Your membership fees have been paid through %(payment_type)s on %(date)s.\n" -" " +" Your membership type does not require payment.\n" +" " +msgstr "" +"\n" +"Je lidmaatschapstype is %(type)s." + +#: templates/profile_overview.html:68 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Your membership fees have been paid through %(payment_type)s on %(date)s.\n" +#| " " +msgid "" +"\n" +" Your membership fees have been paid through %(method)s on %(date)s.\n" +" " msgstr "" "\n" "Je lidmaatschap is betaald via %(payment_type)s op %(date)s." -#: templates/profile_overview.html:69 +#: templates/profile_overview.html:75 msgid "" "\n" " Your membership fee for this year has not yet been paid.\n" @@ -16212,7 +18055,7 @@ msgstr "" "\n" "Je lidmaatschap voor dit jaar is nog niet betaald. Indien je een machtiging hebt gegeven, dan wordt dit automatisch geregeld." -#: templates/profile_overview.html:76 +#: templates/profile_overview.html:82 msgid "" "\n" " Your student status has been verified, or we do not need it for your membership type.\n" @@ -16222,7 +18065,7 @@ msgstr "" " Je studentenstatus is geverifiëerd of we hebben het niet nodig voor jouw lidmaatschapstype.\n" " " -#: templates/profile_overview.html:80 +#: templates/profile_overview.html:86 msgid "" "\n" " Your student status has not been verified, while we do need it for your membership type.\n" @@ -16238,7 +18081,7 @@ msgstr "" "Je kunt contact opnemen met het bestuur om je lidmaatschap vanaf 1 juli om te zetten in een ander type lidmaatschap.\n" " " -#: templates/profile_overview.html:91 +#: templates/profile_overview.html:97 msgid "" "\n" " You have no active membership.\n" @@ -16247,7 +18090,7 @@ msgstr "" "\n" "Je hebt geen actief lidmaatschap." -#: templates/profile_overview.html:148 +#: templates/profile_overview.html:154 msgid "" "\n" " Below you can find an overview of your activism at Inter-Actief.\n" @@ -16258,51 +18101,51 @@ msgstr "" "Hieronder staat een overzicht van je activisme bij Inter-Actief. Wil je actief\n" "worden? Neem dan contact op met de intern." -#: templates/profile_overview.html:160 +#: templates/profile_overview.html:166 msgid "Inter-Actief server access" msgstr "Toegang tot Inter-Actief servers" -#: templates/profile_overview.html:164 +#: templates/profile_overview.html:170 msgid "You are an active member! As an active member you have access to (some of) the Inter-Actief servers." msgstr "Je bent een actief lid! Als actief lid heb je toegang tot (sommige) servers van Inter-Actief." -#: templates/profile_overview.html:168 +#: templates/profile_overview.html:174 msgid "Logging in to the servers is only possible with an SSH key. You can modify your public keys here." msgstr "Inloggen op de servers is alleen mogelijk via SSH-sleutels. Je kan je publieke sleutels hier aanpassen." -#: templates/profile_overview.html:173 templates/profile_overview.html:204 +#: templates/profile_overview.html:179 templates/profile_overview.html:210 msgid "Identifier" msgstr "Identificatie" -#: templates/profile_overview.html:174 +#: templates/profile_overview.html:180 msgid "Public key" msgstr "Publieke sleutel" -#: templates/profile_overview.html:191 +#: templates/profile_overview.html:197 msgid "You have no active SSH public keys." msgstr "Je hebt geen actieve publieke SSH-sleutels." -#: templates/profile_overview.html:201 +#: templates/profile_overview.html:207 msgid "Add new SSH key" msgstr "Nieuwe SSH-key toevoegen" -#: templates/profile_overview.html:206 +#: templates/profile_overview.html:212 msgid "An identifier for the key (max 25 letters or numbers)." msgstr "Een identificatie voor de key (max 25 letters of cijfers)." -#: templates/profile_overview.html:210 +#: templates/profile_overview.html:216 msgid "ssh-ed25519 AAA....." msgstr "ssh-ed25519 AAA....." -#: templates/profile_overview.html:212 +#: templates/profile_overview.html:218 msgid "The SSH public key string." msgstr "De publieke SSH-sleutelreeks." -#: templates/profile_overview.html:215 +#: templates/profile_overview.html:221 msgid "Add new key" msgstr "Nieuwe key toevoegen" -#: templates/profile_overview.html:222 +#: templates/profile_overview.html:228 msgid "" "\n" " If you are in a committee or group that grants you \"sudo\" rights on a server, you will\n" @@ -16316,60 +18159,60 @@ msgstr "" "Je kan op deze knop drukken om dit wachtwoord te resetten, dan zal er een resetlink naar je gemaild worden.\n" " " -#: templates/profile_overview.html:230 +#: templates/profile_overview.html:236 msgid "Reset sudo password" msgstr "Sudo-wachtwoord resetten" -#: templates/profile_overview.html:236 +#: templates/profile_overview.html:242 msgid "You are an active member, but you have no account name! Please contact the board and ask them to add an active members account for you." msgstr "Je bent actief lid, maar je hebt geen accountnaam! Neem contact op met het bestuur zodat ze voor jou een actieveledenaccount aanmaken." -#: templates/profile_overview.html:249 +#: templates/profile_overview.html:255 msgid "The following user account(s) have been found that are related to you." msgstr "De volgende gebruikersaccount(s) zijn gevonden die aan u gerelateerd zijn." -#: templates/profile_overview.html:252 +#: templates/profile_overview.html:258 msgid "You can link another account by logging out, and logging back in with the Google, LinkedIn or GitHub account you want to link." msgstr "U kunt een ander account koppelen door uit te loggen, en weer in te loggen met het Google-, LinkedIn- of GitHub-account dat u wilt koppelen." -#: templates/profile_overview.html:270 templates/profile_overview.html:281 -#: templates/profile_overview.html:312 +#: templates/profile_overview.html:276 templates/profile_overview.html:287 +#: templates/profile_overview.html:318 msgid "unlink" msgstr "ontkoppelen" -#: templates/profile_overview.html:273 +#: templates/profile_overview.html:279 msgid "No TOTP devices registered" msgstr "Geen TOTP-apparaten geregistreerd" -#: templates/profile_overview.html:278 +#: templates/profile_overview.html:284 msgid "Passkeys" msgstr "Passkeys" -#: templates/profile_overview.html:284 +#: templates/profile_overview.html:290 msgid "No passkeys registered" msgstr "Geen passkeys geregistreerd" -#: templates/profile_overview.html:289 +#: templates/profile_overview.html:295 msgid "To register a new TOTP device or passkey, press one of the buttons below. You will receive an e-mail with a link to register the new device." msgstr "Klik op de knoppen hieronder om een nieuw TOTP-apparaat of een nieuwe passkey te registreren. Je krijgt dan een e-mail met een link om het nieuwe apparaat te registreren." -#: templates/profile_overview.html:290 +#: templates/profile_overview.html:296 msgid "Register new TOTP device" msgstr "Registreer nieuw TOTP-apparaat" -#: templates/profile_overview.html:291 +#: templates/profile_overview.html:297 msgid "Register new passkey" msgstr "Registreer nieuwe passkey" -#: templates/profile_overview.html:314 +#: templates/profile_overview.html:320 msgid "Cannot unlink" msgstr "Kan niet ontkoppelen" -#: templates/profile_overview.html:333 +#: templates/profile_overview.html:339 msgid "oAuth2 Apps" msgstr "oAuth2 Apps" -#: templates/profile_overview.html:337 +#: templates/profile_overview.html:343 msgid "" "\n" " Below you can find an overview of the oAuth2 apps you have given permissions to use or edit your personal information.\n" From 8434959ef8f8bddb10b3d16fdb8c6c6a1d82ddee Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 17 Aug 2026 14:58:52 +0000 Subject: [PATCH 38/39] Translated using Weblate (Dutch) Currently translated at 100.0% (3333 of 3333 strings) Translated using Weblate (Dutch) Currently translated at 91.5% (3050 of 3333 strings) Co-authored-by: Kevin Alberts Co-authored-by: Prefill add-on Co-authored-by: Weblate Translate-URL: https://weblate.ia.utwente.nl/projects/amelie/amelie/nl/ Translation: Amelie/Amelie --- locale/nl/LC_MESSAGES/django.mo | Bin 356574 -> 392978 bytes locale/nl/LC_MESSAGES/django.po | 1250 +++++++++++-------------------- 2 files changed, 422 insertions(+), 828 deletions(-) diff --git a/locale/nl/LC_MESSAGES/django.mo b/locale/nl/LC_MESSAGES/django.mo index 3e84ab7ba02fe82a9a74f96f84a14387ecf2424f..0bfba63368736e762a62b42409f0f9fb50697309 100644 GIT binary patch delta 102106 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 z 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 delta 72964 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\n" "Language-Team: Dutch \n" "Language: nl\n" @@ -11,7 +11,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 2026.8\n" +"X-Generator: Weblate 2026.8.1\n" "Used command: ./manage.py makemessages -e py,html,txt,mail\n" "X-Translated-Using: django-rosetta 0.9.9\n" @@ -236,16 +236,13 @@ msgid "Enrolled participants will be emailed when you change the price." msgstr "Ingeschreven deelnemers zullen een e-mail ontvangen als je de prijs aanpast." #: amelie/activities/forms.py:74 -#, fuzzy -#| msgid "Website enrolment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." msgid "Website enrolment must have a positive price. Either turn off the ability to enrol or increase the cost of the activity." -msgstr "Inschrijving via de website heeft een maximum van {0} euro. Of schakel de inschrijfoptie uit, of verlaag de prijs van de activiteit!" +msgstr "Inschrijving via de website moet een positief bedrag kosten. Schakel ofwel de inschrijfoptie uit, of verhoog de prijs van de activiteit!" #: amelie/activities/forms.py:77 -#, fuzzy, python-brace-format -#| msgid "Website enrolment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." +#, python-brace-format msgid "Website enrollment has a maximum of {0} euro. Either turn off the ability to enrol or decrease the cost of the activity." -msgstr "Inschrijving via de website heeft een maximum van {0} euro. Of schakel de inschrijfoptie uit, of verlaag de prijs van de activiteit!" +msgstr "Inschrijving via de website heeft een maximum van {0} euro. Schakel ofwel de inschrijfoptie uit, of verlaag de prijs van de activiteit!" #: amelie/activities/forms.py:81 amelie/activities/models.py:103 msgid "There is no enrollment start date entered." @@ -273,31 +270,27 @@ msgstr "Sla de wachtlijst over en schrijf direct in" #: amelie/activities/forms.py:134 msgid "The amount of current enrollments for this option would exceed the new maximum amount. Please unenroll or edit the affected people first, or modify the maximum for this option." -msgstr "" +msgstr "Het aantal huidige inschrijvingen voor deze optie zou hoger zijn dan het nieuwe maximumaantal. Schrijf a.u.b. eerst mensen uit, of pas het maximum voor deze optie aan." #: amelie/activities/forms.py:147 msgid "The selected amount of (at least) one of the current enrollments for this option exceeds the new maximum amount per person. Please unenroll or edit the affected people first, or modify the maximum for this option." -msgstr "" +msgstr "Het geselecteerde aantal van (ten minste) een van de huidige inschrijvingen voor deze optie is hoger dan het nieuwe maximumaantal per persoon. Schrijf a.u.b. eerst mensen uit, pas hun inschrijving aan, of pas het maximum per persoon voor deze optie aan." #: amelie/activities/forms.py:207 msgid "Sorry, but the last spot has been claimed already!" msgstr "Sorry, maar de laatste plek is al vergeven!" #: amelie/activities/forms.py:229 -#, fuzzy -#| msgid "The amount must be a positive number." msgid "Sorry, but the amount must be a positive number!" -msgstr "Het bedrag moet positief zijn." +msgstr "Sorry, maar het bedrag moet positief zijn!" #: amelie/activities/forms.py:231 msgid "Sorry, but you cannot exceed the maximum amount per person!" msgstr "Sorry, maar je kunt niet over het aantal stuks per persoon!" #: amelie/activities/forms.py:245 -#, fuzzy -#| msgid "Sorry, but there are not enough spots left!" msgid "Sorry, but there are no spots left!" -msgstr "Sorry, maar er zijn niet genoeg plekken over!" +msgstr "Sorry, maar er zijn geen plekken over!" #: amelie/activities/forms.py:247 msgid "Sorry, but there are not enough spots left!" @@ -310,10 +303,8 @@ msgstr "Sorry, maar er zijn niet genoeg plekken over!" #: amelie/personal_tab/templates/info/payments.html:79 #: amelie/personal_tab/templates/info/transaction_settlement_info.html:29 #: amelie/personal_tab/templates/lists/payment_transactions.html:13 -#, fuzzy -#| msgid "Payment Method" msgid "Payment method" -msgstr "Betalingsmethode" +msgstr "Betaalmethode" #: amelie/activities/forms.py:375 msgid "Photographer" @@ -544,10 +535,9 @@ msgid "Facebook event id, numerical value in the facebook event url." msgstr "Facebook event id, numerieke waarde in de facebook event url." #: amelie/activities/models.py:243 -#, fuzzy, python-brace-format -#| msgid "Unenrollment for {activity} is not possible." +#, python-brace-format msgid "Enrollment for {activity} is not possible." -msgstr "Uitschrijven voor {activity} is niet mogelijk." +msgstr "Inschrijven voor {activity} is niet mogelijk." #: amelie/activities/models.py:247 #, python-brace-format @@ -600,14 +590,12 @@ msgid "The unenrollment term for {activity} has already passed." msgstr "De uitschrijfperiode voor {activity} is al voorbij." #: amelie/activities/models.py:350 -#, fuzzy -#| msgid "Activities iCal feed" msgid "Activity was cancelled" -msgstr "Activiteiten iCal-feed" +msgstr "Activiteit is geannuleerd" #: amelie/activities/models.py:365 msgid "Activity has already started, it cannot be canceled any more." -msgstr "" +msgstr "Activiteit is al begonnen, en kan niet meer worden geannuleerd." #: amelie/activities/models.py:394 #: amelie/activities/templates/activities/enrollmentoption_list.html:41 @@ -828,10 +816,9 @@ msgid "{0:.1f}% certain" msgstr "{0:.1f}% zeker" #: amelie/activities/templates/activities/activity_cancelled.mail:3 -#, fuzzy, python-format -#| msgid "Cancellation of " +#, python-format msgid "Cancellation of %(activity)s" -msgstr "Annulering van " +msgstr "Annulering van %(activity)s" #: amelie/activities/templates/activities/activity_cancelled.mail:4 #: amelie/claudia/templates/claudia/account_deleted.mail:5 @@ -840,30 +827,26 @@ msgid "Dear %(name)s," msgstr "Beste %(name)s," #: amelie/activities/templates/activities/activity_cancelled.mail:5 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| "You receive this email because you were enrolled for %(summary)s. This activity has been cancelled.\n" +#, python-format msgid "" "\n" "You receive this email because you were enrolled for %(activity)s. This activity has been cancelled.\n" msgstr "" "\n" -"Je ontvangt deze e-mail omdat je bent ingeschreven voor %(summary)s. Deze activiteit is geannuleerd.\n" +"Je ontvangt deze e-mail omdat je bent ingeschreven voor %(activity)s. Deze activiteit is geannuleerd.\n" #: amelie/activities/templates/activities/activity_cancelled.mail:8 -#, fuzzy, python-format -#| msgid "The total costs are %(costs)s euro. Payment method: %(paymentmethod)s." +#, python-format msgid "The total costs were %(costs)s euro." -msgstr "De kosten zijn totaal %(costs)s euro. Betaalwijze: %(paymentmethod)s." +msgstr "De kosten waren totaal %(costs)s euro." #: amelie/activities/templates/activities/activity_cancelled.mail:9 msgid "These costs will be refunded on your personal tab." -msgstr "" +msgstr "Deze kosten zullen worden teruggeboekt op je streeplijstsaldo." #: amelie/activities/templates/activities/activity_cancelled.mail:10 msgid "If you already paid for this activity, please come by the association room or contact the treasurer to re-collect your payment. If you had not paid yet you do not need to do anything." -msgstr "" +msgstr "Als je al betaald had voor deze activiteit, kom dan langs de verenigingskamer of neem contact op met de penningmeester om je betaling terug te krijgen. Als je nog niet had betaald dan hoef je niks te doen." #: amelie/activities/templates/activities/activity_cancelled.mail:11 #: amelie/activities/templates/activities/activity_cancelled_from_waiting_list.mail:7 @@ -941,20 +924,19 @@ msgstr "Je ontvangt deze e-mail omdat je bent ingeschreven voor %(summary)s. Dez #: amelie/activities/templates/activities/activity_enrolled.mail:9 #: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:9 -#, fuzzy, python-format -#| msgid "The total costs are %(costs)s euro. Payment method: %(paymentmethod)s." +#, python-format msgid "The total costs are %(costs)s euro." -msgstr "De kosten zijn totaal %(costs)s euro. Betaalwijze: %(paymentmethod)s." +msgstr "De kosten zijn totaal %(costs)s euro." #: amelie/activities/templates/activities/activity_enrolled.mail:10 #: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:10 msgid "These costs will be debited from your bank account via your direct debit mandate." -msgstr "" +msgstr "Deze kosten zullen worden afgeschreven van je bankrekening via je incassomachtiging." #: amelie/activities/templates/activities/activity_enrolled.mail:10 #: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:10 msgid "These costs will need to be paid manually by you, either at the activity or at the board." -msgstr "" +msgstr "Deze kosten zullen door jou handmatig betaald moeten worden, ofwel bij de activiteit of bij het bestuur." #: amelie/activities/templates/activities/activity_enrolled.mail:12 #: amelie/activities/templates/activities/activity_enrolled_from_waiting_list.mail:12 @@ -2028,10 +2010,8 @@ msgstr "Data-exportstatistieken" #: amelie/activities/templates/activity_confirm_cancellation.html:4 #: amelie/activities/templates/activity_confirm_cancellation.html:8 -#, fuzzy -#| msgid "Confirmation cancel" msgid "Confirmation undo cancel" -msgstr "Bevestig verwijdering" +msgstr "Bevestig ongedaan maken van annulering" #: amelie/activities/templates/activity_confirm_cancellation.html:4 #: amelie/activities/templates/activity_confirm_cancellation.html:8 @@ -2039,36 +2019,30 @@ msgid "Confirmation cancel" msgstr "Bevestig verwijdering" #: amelie/activities/templates/activity_confirm_cancellation.html:12 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Are you sure you want to cancel %(obj)s?\n" -#| " " +#, python-format msgid "" "\n" " Are you sure you want to undo the cancelation of %(obj)s?\n" " " msgstr "" "\n" -"Weet je zeker dat je %(obj)s wilt annuleren?" +" Weet je zeker dat je het annuleren van %(obj)s ongedaan wilt maken?\n" +" " #: amelie/activities/templates/activity_confirm_cancellation.html:16 msgid "The event will be available again, but enrollments will stay closed and need to be manually re-opened." -msgstr "" +msgstr "De activiteit zal weer beschikbaar zijn, maar de inschrijving zal dicht blijven en zal handmatig opnieuw geopend moeten worden." #: amelie/activities/templates/activity_confirm_cancellation.html:19 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Are you sure you want to cancel %(obj)s?\n" -#| " " +#, python-format msgid "" "\n" " Are you sure you want to cancel %(obj)s?\n" " " msgstr "" "\n" -"Weet je zeker dat je %(obj)s wilt annuleren?" +" Weet je zeker dat je %(obj)s wilt annuleren?\n" +" " #: amelie/activities/templates/activity_confirm_cancellation.html:23 msgid "All enrollments will be cancelled, the transactions will be undone, and all the participants will receive an email." @@ -2108,178 +2082,135 @@ msgid "Confirmation unenrollment" msgstr "Bevestiging uitschrijving" #: amelie/activities/templates/activity_confirm_unenrollment.html:12 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Are you sure you want to unenroll %(person)s for %(obj)s?\n" -#| " " +#, python-format msgid "" "\n" " Are you sure you want to unenroll %(person)s for %(obj)s?\n" " " msgstr "" "\n" -"Weet je zeker dat je %(person)s wil uitschrijven voor %(obj)s?" +" Weet je zeker dat je %(person)s wil uitschrijven voor %(obj)s?\n" +" " #: amelie/activities/templates/activity_confirm_unenrollment.html:16 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Are you sure you want to unenroll %(person)s for %(obj)s?\n" -#| " " +#, python-format msgid "" "\n" " Are you sure you want to unenroll for %(obj)s?\n" " " msgstr "" "\n" -"Weet je zeker dat je %(person)s wil uitschrijven voor %(obj)s?" +" Weet je zeker dat je je wilt uitschrijven voor %(obj)s?\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:7 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Enroll person for %(name)s\n" -#| " " +#, python-format msgid "" "\n" " Update enrollment of %(person)s for %(summary)s\n" " " msgstr "" "\n" -"Persoon inschrijven voor %(name)s" +" Inschrijving van %(person)s voor %(summary)s aanpassen\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:11 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Add credit for %(person)s.\n" -#| " " +#, python-format msgid "" "\n" " Update enrollment for %(summary)s\n" " " msgstr "" "\n" -"Tentamenkoekentegoed toevoegen voor %(person)s" +" Inschrijving voor %(summary)s aanpassen\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:17 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Enroll person for %(name)s\n" -#| " " +#, python-format msgid "" "\n" " Enroll %(person)s for %(summary)s\n" " " msgstr "" "\n" -"Persoon inschrijven voor %(name)s" +" %(person)s inschrijven voor %(summary)s\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:21 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Enroll person for %(name)s\n" -#| " " +#, python-format msgid "" "\n" " Enroll for %(summary)s\n" " " msgstr "" "\n" -"Persoon inschrijven voor %(name)s" +" Inschrijven voor %(summary)s\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:35 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Last %(count)s transactions\n" -#| " " +#, python-format msgid "" "\n" " Update enrollment of %(person)s for %(summary)s\n" " " msgstr "" "\n" -"Laatste %(count)s toegevoegde transacties." +" Inschrijving van %(person)s voor %(summary)s aanpassen\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:39 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " 1 person signed up.\n" -#| " " -#| msgid_plural "" -#| "\n" -#| " %(sign_ups)s people signed up.\n" -#| " " +#, python-format msgid "" "\n" " Update enrollment for %(summary)s\n" " " msgstr "" "\n" -" 1 persoon is ingeschreven.\n" +" Inschrijving voor %(summary)s aanpassen\n" " " #: amelie/activities/templates/activity_enrollment_form.html:45 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Enroll person for %(name)s\n" -#| " " +#, python-format msgid "" "\n" " Enroll %(person)s for %(summary)s\n" " " msgstr "" "\n" -"Persoon inschrijven voor %(name)s" +" %(person)s inschrijven voor %(summary)s\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:49 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " %(time)s ago\n" -#| " " +#, python-format msgid "" "\n" " Enroll for %(summary)s\n" " " msgstr "" "\n" -" %(time)s geleden\n" -" " +" Inschrijven voor %(summary)s\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:60 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| "\t\t\t\t\t\t \tWith this form, you will enroll %(name)s for the activity.\n" -#| "\t\t\t\t\t\t" +#, python-format msgid "" "\n" "\t\t\t\t\t\t \tWith this form, you will update the enrollment of %(name)s for the activity.\n" "\t\t\t\t\t\t" msgstr "" "\n" -"Met dit formulier schrijf je %(name)s in voor de activiteit." +"\t\t\t\t\t\t \tMet dit formulier pas je de inschrijving voor de activiteit van %(name)s aan.\n" +"\t\t\t\t\t\t" #: amelie/activities/templates/activity_enrollment_form.html:64 -#, fuzzy -#| msgid "" -#| "\n" -#| "\t\t\t\t\t\t \tClick \"Enroll\" below this form to confirm your enrollment.\n" -#| "\t\t\t\t\t\t" msgid "" "\n" "\t\t\t\t\t\t \tClick \"Update\" below this form to save the changes to your enrollment.\n" "\t\t\t\t\t\t" msgstr "" "\n" -"Klik onderaan dit formulier op inschrijven om je inschrijving te bevestigen." +"\t\t\t\t\t\t \tKlik onderaan dit formulier op \"Aanpassen\" om de wijzigingen aan je inschrijving op te slaan.\n" +"\t\t\t\t\t\t" #: amelie/activities/templates/activity_enrollment_form.html:70 #, python-format @@ -2305,10 +2236,8 @@ msgid "The enrollment fee is" msgstr "De inschrijfkosten bedragen" #: amelie/activities/templates/activity_enrollment_form.html:88 -#, fuzzy -#| msgid "Unfortunately, there are no more openings. You will be enrolled on the waiting list." msgid "There are no more openings. The person will be enrolled on the waiting list unless you override this." -msgstr "Helaas, maar er is geen beschikbare plek meer. Je kunt je wel inschrijven op de wachtlijst." +msgstr "Er is geen beschikbare plek meer. De persoon zal worden ingeschreven op de wachtlijst, tenzij je dit aanpast." #: amelie/activities/templates/activity_enrollment_form.html:90 msgid "Unfortunately, there are no more openings. You will be enrolled on the waiting list." @@ -2316,39 +2245,33 @@ msgstr "Helaas, maar er is geen beschikbare plek meer. Je kunt je wel inschrijve #: amelie/activities/templates/activity_enrollment_form.html:103 msgid "The activity has a few enrollment options. Please fill these out before you enroll." -msgstr "" +msgstr "De activiteit heeft een aantal inschrijfopties. Vul deze in voordat je jezelf inschrijft." #: amelie/activities/templates/activity_enrollment_form.html:106 msgid "Warning: some of these options may cost extra!" -msgstr "" +msgstr "Let op: sommige van deze opties kunnen extra kosten!" #: amelie/activities/templates/activity_enrollment_form.html:144 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " It is assumed that %(name)s has given their permission to be enrolled.\n" -#| " " +#, python-format msgid "" "\n" " It is assumed that %(name)s has given their permission for their enrollment to be modified.\n" " " msgstr "" "\n" -"Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om zich te laten inschrijven." +" Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om hun inschrijving aan te passen.\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:148 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " It is assumed that %(name)s has given their permission to be enrolled.\n" -#| " " +#, python-format msgid "" "\n" " It is assumed that %(name)s has given their permission to be enrolled.\n" " " msgstr "" "\n" -"Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om zich te laten inschrijven." +" Er wordt vanuit gegaan dat %(name)s toestemming heeft gegeven om zich te laten inschrijven.\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:159 msgid "" @@ -2406,12 +2329,7 @@ msgid "Total" msgstr "Totaal" #: amelie/activities/templates/activity_enrollment_form.html:175 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " If your print is for a committee, select it in the form above.\n" -#| " In that case the print will be free.\n" -#| " " +#, python-format msgid "" "\n" " The costs will be deducted from %(name)s's personal tab\n" @@ -2419,18 +2337,11 @@ msgid "" " " msgstr "" "\n" -" Als je print voor een commissie, selecteer die dan in het bovenstaande formulier.\n" -"In dat geval zal de print gratis zijn.\n" -" " +" Deze kosten worden afgeschreven van %(name)s's streeplijst en zullen betaald worden via automatische incasso.\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:182 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Warning: This activity already has participants.\n" -#| " It is therefore impossible to add or alter options for enrollment.\n" -#| " Changing prices and/or texts will only influence people who have yet to enroll!\n" -#| " " +#, python-format msgid "" "\n" " %(name)s has no direct debit mandate for activities.\n" @@ -2439,7 +2350,10 @@ msgid "" " " msgstr "" "\n" -"Let op: Activiteit heeft al deelnemers. Het is daarom niet mogelijk inschrijfopties toe te voegen of te verwijderen. Wijzigen van prijzen en/of hebben alleen invloed op de mensen die zich nog gaan inschrijven!" +" %(name)s heeft geen incassomachtiging voor activiteiten. \n" +"De kosten zullen van diens streeplijstsaldo afgeschreven worden \n" +"en moeten handmatig betaald worden, ofwel bij de activiteit of bij het bestuur.\n" +" " #: amelie/activities/templates/activity_enrollment_form.html:193 msgid "Update" @@ -2472,13 +2386,6 @@ msgstr "" "Inschrijven voor %(name)s" #: amelie/activities/templates/activity_enrollment_mandate.html:20 -#, fuzzy -#| msgid "" -#| "\n" -#| "\t\t\t\t \tUnfortunately, you cannot enroll for this activity, because it has an entrance fee, and you have\n" -#| " not signed a direct debit mandate for consumptions and activities. Without this authorization, we\n" -#| " can not withdraw money from your account. Please ask the board for a mandate form.\n" -#| "\t\t\t\t" msgid "" "\n" "\t\t\t\t \tUnfortunately, you cannot enroll for this activity, because it has an entrance fee, or you have\n" @@ -2488,18 +2395,12 @@ msgid "" "\t\t\t\t" msgstr "" "\n" -"Je kunt je helaas niet inschrijven voor deze activiteit omdat aan deze activiteit kosten verbonden\n" -"zijn en je geen machtiging getekend hebt inzake consumpties en activiteiten. Zonder deze machtiging\n" -"is het niet mogelijk om eventuele kosten af te schrijven. Vraag bij het bestuur om een\n" -"machtigingsformulier." +"\t\t\t\t \tJe kunt je helaas niet inschrijven voor deze activiteit omdat aan deze activiteit kosten verbonden zijn, \n" +"of je hebt een betaalde inschrijfoptie gekozen. Je hebt geen machtiging getekend voor consumpties en activiteiten. \n" +"Zonder deze machtiging is het niet mogelijk om eventuele kosten af te schrijven. Vraag bij het bestuur om een machtigingsformulier.\n" +"\t\t\t\t" #: amelie/activities/templates/activity_enrollment_mandate.html:29 -#, fuzzy -#| msgid "" -#| "\n" -#| "\t\t Would you prefer not to sign a direct debit mandate? Please contact the board or organizing committee\n" -#| " and pay in cash.\n" -#| "\t\t " msgid "" "\n" "\t\t Would you prefer not to sign a direct debit mandate? Please contact the board or organizing\n" @@ -2507,7 +2408,7 @@ msgid "" "\t\t " msgstr "" "\n" -"\t\t Liever geen machtiging tekenen? Neem dan contact op met het bestuur of de organiserende commissie en betaal contant.\n" +"\t\t Liever geen machtiging tekenen? Neem dan contact op met het bestuur of de organiserende commissie om te kijken of het mogelijk is om contant of met PIN te betalen voor deze activiteit.\n" "\t\t " #: amelie/activities/templates/activity_enrollment_person_search.html:10 @@ -2531,16 +2432,14 @@ msgstr "" "Persoon inschrijven voor %(name)s" #: amelie/activities/templates/activity_enrollment_print.html:19 -#, fuzzy, python-format -#| msgid "Enrollment for courses starts" +#, python-format msgid "Enrollments for %(name)s (%(date)s)" -msgstr "Inschrijving vakken start" +msgstr "Inschrijvingen voor %(name) (%(date)s)" #: amelie/activities/templates/activity_enrollment_print.html:20 -#, fuzzy, python-format -#| msgid "Signature on %(date)s:" +#, python-format msgid "(printed on %(date)s)" -msgstr "Was getekend op %(datum)s:" +msgstr "(geprint op %(date)s)" #: amelie/activities/templates/activity_enrollment_print.html:23 msgid "This enrollment list contains personal data. Please do not leave this lying around somewhere and make sure that it is destroyed after use. If you find this list somewhere, please give it to the board of Inter-Actief." @@ -2561,7 +2460,7 @@ msgstr "Betaling" #: amelie/activities/templates/includes/activity_participants_table.html:65 #: amelie/activities/templates/includes/activity_participants_table.html:137 msgid "N/A (free)" -msgstr "" +msgstr "n.v.t. (gratis)" #: amelie/activities/templates/activity_enrollment_print.html:59 #: amelie/activities/templates/includes/activity_participants_table.html:79 @@ -2570,7 +2469,7 @@ msgstr "" #: amelie/members/templates/person_membership.html:84 #: amelie/members/templates/person_membership_payments.html:25 msgid "Paid" -msgstr "" +msgstr "Betaald" #: amelie/activities/templates/activity_enrollment_print.html:62 #: amelie/activities/templates/activity_enrollment_print.html:64 @@ -2582,15 +2481,13 @@ msgstr "" #: amelie/members/templates/person_membership.html:88 #: amelie/members/templates/person_membership.html:90 msgid "To be debited" -msgstr "" +msgstr "Nog te incasseren" #: amelie/activities/templates/activity_enrollment_print.html:67 #: amelie/activities/templates/includes/activity_enrollment_overview.html:177 #: amelie/members/templates/person_membership.html:127 -#, fuzzy -#| msgid "No payment" msgid "Needs manual payment" -msgstr "Geen betaling" +msgstr "Handmatige betaling vereist" #: amelie/activities/templates/activity_enrollment_print.html:69 #: amelie/activities/templates/includes/activity_participants_table.html:87 @@ -2598,16 +2495,14 @@ msgstr "Geen betaling" #: amelie/activities/templates/includes/activity_participation_payments.html:27 #: amelie/members/templates/person_membership.html:92 #: amelie/members/templates/person_membership_payments.html:27 -#, fuzzy -#| msgid "Has not paid yet" msgid "Not paid yet" -msgstr "Heeft nog niet betaald" +msgstr "Nog niet betaald" #: amelie/activities/templates/activity_enrollment_print.html:71 #: amelie/activities/templates/includes/activity_enrollment_overview.html:178 #, python-format msgid "Open amount: € %(costs)s" -msgstr "" +msgstr "Open bedrag: € %(costs)s" #: amelie/activities/templates/activity_enrollment_print.html:83 #: amelie/activities/templates/includes/activity_enrollment_overview.html:80 @@ -3233,89 +3128,67 @@ msgstr "Deze deelnemer wordt 18 tijdens de activiteit!" #: amelie/activities/templates/includes/activity_participants_table.html:69 msgid "For some reason this free participation has unpaid payments! This should not happen." -msgstr "" +msgstr "Om de een of andere reden heeft deze gratis inschrijving onbetaalde transacties! Dit zou niet mogen gebeuren." #: amelie/activities/templates/includes/activity_participants_table.html:71 #: amelie/activities/templates/includes/activity_participants_table.html:91 #: amelie/members/templates/person_membership.html:76 #: amelie/members/templates/person_membership.html:96 -#, fuzzy -#| msgid "Execute payment" msgid "Inspect payments" -msgstr "Betaling invoeren" +msgstr "Betalingen bekijken" #: amelie/activities/templates/includes/activity_participants_table.html:96 #: amelie/members/templates/person_membership.html:101 -#, fuzzy -#| msgid "No transactions on personal tab." msgid "No transaction! Contact the WWW." -msgstr "Geen streeplijsttransacties." +msgstr "Geen transactie! Neem contact op met de WWW." #: amelie/activities/templates/includes/activity_participants_table.html:100 #: amelie/members/templates/person_membership.html:105 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " (last used: %(last_used_date)s)\n" -#| " " +#, python-format msgid "" "\n" " Debited on %(date)s\n" " " msgstr "" "\n" -" (laatst gebruikt: %(last_used_date)s)\n" -" " +" Afgeschreven op %(date)s\n" +" " #: amelie/activities/templates/includes/activity_participants_table.html:106 #: amelie/members/templates/person_membership.html:111 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " (last used: %(last_used_date)s)\n" -#| " " +#, python-format msgid "" "\n" " %(method)s on %(date)s\n" " " msgstr "" "\n" -" (laatst gebruikt: %(last_used_date)s)\n" -" " +" (%(method)s op %(date)s)\n" +" " #: amelie/activities/templates/includes/activity_participants_table.html:112 #: amelie/members/templates/person_membership.html:117 msgid "Unknown payment! Contact the WWW." -msgstr "" +msgstr "Onbekende betaling! Neem contact op met de WWW." #: amelie/activities/templates/includes/activity_participants_table.html:117 #: amelie/members/templates/person_membership.html:122 msgid "Previous attempt was reversed" -msgstr "" +msgstr "Vorige poging is teruggeboekt" #: amelie/activities/templates/includes/activity_participants_table.html:121 #: amelie/activities/templates/includes/activity_participants_table.html:151 #, python-format msgid "Needs manual payment (€ %(costs)s)" -msgstr "" +msgstr "Handmatige betaling vereist (€ %(costs)s)" #: amelie/activities/templates/includes/activity_participants_table.html:128 #: amelie/members/templates/person_membership.html:135 -#, fuzzy -#| msgid "Confirmation payment" msgid "Enter manual payment" -msgstr "Bevestigen betaling" +msgstr "Handmatige betaling invoeren" #: amelie/activities/templates/includes/activity_participants_table.html:188 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " ...and %(count)s Anonymous member.\n" -#| " " -#| msgid_plural "" -#| "\n" -#| " ...and %(count)s Anonymous members.\n" -#| " " +#, python-format msgid "" "\n" " ...and %(count)s Anonymous member.\n" @@ -3326,33 +3199,28 @@ msgid_plural "" " " msgstr[0] "" "\n" -" ...en %(count)s Anoniem lid.\n" -" " +" ...en %(count)s Anoniem lid.\n" +" " msgstr[1] "" "\n" -" ...en %(count)s Anonieme leden.\n" -" " +" ...en %(count)s Anonieme leden.\n" +" " #: amelie/activities/templates/includes/activity_participation_create_payment.html:5 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Add credit for %(person)s.\n" -#| " " +#, python-format msgid "" "\n" " Create manual payment for %(participation)s\n" " " msgstr "" "\n" -"Tentamenkoekentegoed toevoegen voor %(person)s" +" Handmatige betaling aanmaken voor %(participation)s\n" +" " #: amelie/activities/templates/includes/activity_participation_create_payment.html:10 #: amelie/members/templates/person_membership_create_payment.html:10 -#, fuzzy -#| msgid "You are on the waiting list for this event with the following options:" msgid "You are creating a manual payment that will cover the following transactions:" -msgstr "Je hebt je ingeschreven voor deze activiteit op de wachtlijst met de volgende inschrijfopties:" +msgstr "Je maakt een handmatige betaling aan voor de volgende transacties:" #: amelie/activities/templates/includes/activity_participation_create_payment.html:16 #: amelie/activities/templates/includes/activity_participation_payments.html:35 @@ -3424,65 +3292,54 @@ msgstr "Omschrijving" #: amelie/activities/templates/includes/activity_participation_payments.html:48 #: amelie/members/templates/person_membership_create_payment.html:28 #: amelie/members/templates/person_membership_payments.html:48 -#, fuzzy -#| msgid "Print membership forms (opens in new tab)" msgid "Transaction details (opens in new tab)" -msgstr "Lidmaatschapsformulieren printen (opent in nieuwe tab)" +msgstr "Transactiedetails (opent nieuw tabblad)" #: amelie/activities/templates/includes/activity_participation_create_payment.html:39 #: amelie/members/templates/person_membership_create_payment.html:39 msgid "No transactions found. This is probably wrong! Please contact the WWW." -msgstr "" +msgstr "Geen transacties gevonden. Dit klopt waarschijnlijk niet! Neem contact op met de WWW." #: amelie/activities/templates/includes/activity_participation_create_payment.html:52 #: amelie/members/templates/person_membership_create_payment.html:52 msgid "Please select the payment method that was used for the payment." -msgstr "" +msgstr "Selecteer de betaalmethode die gebruikt is voor de betaling." #: amelie/activities/templates/includes/activity_participation_create_payment.html:63 #: amelie/members/templates/person_membership_create_payment.html:63 -#, fuzzy -#| msgid "Create page" msgid "Create payment" -msgstr "Pagina aanmaken" +msgstr "Betaling toevoegen" #: amelie/activities/templates/includes/activity_participation_payments.html:8 #: amelie/members/templates/person_membership_payments.html:8 msgid "Payment created. Please check the transactions below for correctness." -msgstr "" +msgstr "Betaling aangemaakt. Controleer onderstaande transacties op juistheid." #: amelie/activities/templates/includes/activity_participation_payments.html:14 #: amelie/members/templates/person_membership_payments.html:14 -#, fuzzy -#| msgid "Invalid action. Please try again." msgid "No unpaid transactions found. Please re-check and try again." -msgstr "Ongeldige actie. Probeer het opnieuw." +msgstr "Geen onbetaalde transacties gevonden. Controleer en probeer het opnieuw." #: amelie/activities/templates/includes/activity_participation_payments.html:20 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Add credit for %(person)s.\n" -#| " " +#, python-format msgid "" "\n" " Payments for %(participation)s\n" " " msgstr "" "\n" -"Tentamenkoekentegoed toevoegen voor %(person)s" +" Betalingen voor %(participation)s\n" +" " #: amelie/activities/templates/includes/activity_participation_payments.html:38 #: amelie/members/templates/person_membership_payments.html:38 #: amelie/personal_tab/templates/info/transaction_information.html:19 -#, fuzzy -#| msgid "Stay logged in" msgid "Settled in" -msgstr "Blijf ingelogd" +msgstr "Afgehandeld in" #: amelie/activities/templates/includes/activity_participation_payments.html:62 msgid "No transactions found. If this is a paid activity, this is probably wrong! Please contact the WWW." -msgstr "" +msgstr "Geen transacties gevonden! Als dit een betaalde activiteit is dan klopt dit waarschijnlijk niet! Neem dan contact op met de WWW." #: amelie/activities/templates/includes/activity_participation_payments.html:70 #: amelie/members/templates/person_mandate_retrieve_signed.html:18 @@ -3653,38 +3510,31 @@ msgid "We were unable to cancel {obj}, since it has already started." msgstr "Het annuleren van {obj} is niet meer mogelijk, omdat deze al is begonnen." #: amelie/activities/views.py:387 -#, fuzzy -#| msgid "You are not allowed to edit this activity" msgid "You are not allowed to access this activity page for other people." -msgstr "Je mag deze activiteit niet bewerken" +msgstr "Je hebt geen toegang tot deze activiteitenpagina voor andere personen." #: amelie/activities/views.py:390 msgid "Not implemented on base enrollment view." -msgstr "" +msgstr "Niet geimplementeerd voor de basis inschrijvingsview." #: amelie/activities/views.py:452 -#, fuzzy -#| msgid "You are not allowed to edit this activity" msgid "You are not allowed to enroll other people for this activity." -msgstr "Je mag deze activiteit niet bewerken" +msgstr "Je hebt geen toegang om andere personen in te schrijven voor deze activiteit." #: amelie/activities/views.py:493 amelie/activities/views.py:690 -#, fuzzy, python-brace-format -#| msgid "{person} is now enrolled for {waiting_list} {activity}." +#, python-brace-format msgid "{person} is now enrolled for the waitinglist of {activity}." -msgstr "{person} is nu ingeschreven voor {waiting_list} {activity}." +msgstr "{person} is nu ingeschreven voor de wachtlijst van {activity}." #: amelie/activities/views.py:495 amelie/activities/views.py:692 -#, fuzzy, python-brace-format -#| msgid "{person} is now unenrolled for {activity}." +#, python-brace-format msgid "{person} is now enrolled for {activity}." -msgstr "{person} is nu uitgeschreven voor {activity}." +msgstr "{person} is nu ingeschreven voor {activity}." #: amelie/activities/views.py:499 amelie/activities/views.py:696 -#, fuzzy, python-brace-format -#| msgid "You are now enrolled for {waiting_list} {activity}." +#, python-brace-format msgid "You are now enrolled for the waitinglist of {activity}." -msgstr "Je bent nu ingeschreven voor {waiting_list} {activity}." +msgstr "Je bent nu ingeschreven op de wachtlijst voor {activity}." #: amelie/activities/views.py:501 amelie/activities/views.py:698 #, python-brace-format @@ -3693,13 +3543,11 @@ msgstr "Je bent nu ingeschreven voor {activity}." #: amelie/activities/views.py:562 amelie/activities/views.py:731 msgid "Your changes would make the costs of your participation negative! Unfortunately that's not allowed, so the change was blocked. Please contact the board or the organizing committee if you think this is a mistake." -msgstr "" +msgstr "Je wijzigingen zouden de kosten van je inschrijving negatief maken! Helaas is dat niet toegestaan, dus de wijziging is geblokkeerd. Neem a.u.b. contact op met het bestuur of de organiserende commissie als je denkt dat dit een fout is." #: amelie/activities/views.py:598 -#, fuzzy -#| msgid "You are not allowed to edit this activity" msgid "You are not allowed to unenroll other people for this activity." -msgstr "Je mag deze activiteit niet bewerken" +msgstr "Je hebt geen toegang om andere personen uit te schrijven voor deze activiteit." #: amelie/activities/views.py:620 #, python-brace-format @@ -3717,14 +3565,12 @@ msgid "{person} has been promoted from the waiting list for {activity}." msgstr "{person} is gepromoveerd van de wachtlijst naar bevestigde deelname voor {activity}." #: amelie/activities/views.py:651 -#, fuzzy -#| msgid "You are not allowed to edit this activity" msgid "You are not allowed to edit enrollments of other people for this activity." -msgstr "Je mag deze activiteit niet bewerken" +msgstr "Je hebt geen toegang om inschrijvingen van andere personen aan te passen voor deze activiteit." #: amelie/activities/views.py:710 msgid "Participation not found while editing enrollment." -msgstr "" +msgstr "Deelname niet gevonden tijdens aanpassen van inschrijving." #: amelie/activities/views.py:757 amelie/activities/views.py:1045 msgid "No photo" @@ -3765,7 +3611,7 @@ msgstr "Je hebt geen Nederlandse samenvatting/omschrijving ingevuld." #: amelie/activities/views.py:1194 msgid "Editing the activity price to this value would make the minimum possible price for this activity negative! This is not allowed. Please modify the pricing and limits of the activity or the enrollment options first." -msgstr "" +msgstr "Het aanpassen van de prijs van de activiteit zou ervoor zorgen dat de minimaal mogelijke prijs van deze activiteit negatief wordt! Dit is niet toegestaan. Pas a.u.b. eerst de prijs of limieten van de activiteit of de inschrijfopties aan." #: amelie/activities/views.py:1207 msgid "start date was changed" @@ -3782,7 +3628,7 @@ msgstr "Activiteit '{activity}' is aangepast (terugboeken van oude transactie) ( #: amelie/activities/views.py:1238 msgid "Editing the activity in this way would make the costs of (at least) one of the current enrollments negative! This is not allowed. Please unenroll the affected people, or modify the pricing and limits of the activity or the enrollment options first." -msgstr "" +msgstr "Het aanpassen van de activiteit op deze manier zou ervoor zorgen dat de kosten van (ten minste) een van de huidige inschrijvingen van deze activiteit negatief wordt! Dit is niet toegestaan. Schrijf de betrokken mensen eerst uit, of pas eerst de prijs of limieten van de activiteit of de inschrijfopties aan." #: amelie/activities/views.py:1243 #, python-brace-format @@ -3791,7 +3637,7 @@ msgstr "Activiteit '{activity}' is aangepast (toevoegen van nieuwe transactie) ( #: amelie/activities/views.py:1318 msgid "Adding this option would make the minimum possible price for this activity negative! This is not allowed. Please modify the pricing or limits of this or other enrollment options first." -msgstr "" +msgstr "Het toevoegen van deze optie zou ervoor zorgen dat de minimaal mogelijke prijs van deze activiteit negatief wordt! Dit is niet toegestaan. Pas a.u.b. eerst de prijs of limieten van deze of andere inschrijfopties aan." #: amelie/activities/views.py:1332 amelie/activities/views.py:1342 msgid "This activity already has enrollments, therefore options can no longer be added!" @@ -3799,7 +3645,7 @@ msgstr "Deze activiteit heeft inschrijvingen, er kunnen geen opties toegevoegd w #: amelie/activities/views.py:1374 msgid "Editing this option in this way would make the minimum possible price for this activity negative! This is not allowed. Please modify the pricing or limits of this or other enrollment options first." -msgstr "" +msgstr "Het aanpassen van deze optie op deze manier zou ervoor zorgen dat de minimaal mogelijke prijs van deze activiteit negatief wordt! Dit is niet toegestaan. Pas a.u.b. eerst de prijs of limieten van deze of andere inschrijfopties aan." #: amelie/activities/views.py:1384 msgid "enrollment option price was changed" @@ -3812,7 +3658,7 @@ msgstr "Een inschrijfoptie voor activiteit '{activity}' is aangepast (terugboeke #: amelie/activities/views.py:1409 msgid "Editing this option in this way would make the costs of (at least) one of the current enrollments negative! This is not allowed. Please unenroll the affected people, or modify the pricing or limits of this or other enrollment options first." -msgstr "" +msgstr "Het aanpassen van deze optie op deze manier zou ervoor zorgen dat de kosten van (ten minste) een van de huidige inschrijvingen van deze activiteit negatief wordt! Dit is niet toegestaan. Schrijf de betrokken mensen eerst uit, of pas eerst de prijs of limieten van deze of andere inschrijfopties aan." #: amelie/activities/views.py:1414 #, python-brace-format @@ -4035,12 +3881,12 @@ msgstr "Een korte beschrijving van deze activiteit (altijd in het Engels)." #: amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py:170 #, python-brace-format msgid "Manual payment required for enrollment to {event} on {date} (migrated)" -msgstr "" +msgstr "Handmatige betaling vereist voor inschrijving voor {event} op {date} (gemigreerd)" #: amelie/calendar/migrations/0009_remove_participation_cash_payment_made_and_more.py:190 #, python-brace-format msgid "Manual payment for enrollment to {event} on {date} (migrated)" -msgstr "" +msgstr "Handmatige betaling voor inschrijving voor {event} op {date} (gemigreerd)" #: amelie/calendar/models.py:30 amelie/data_export/models.py:31 #: amelie/education/models.py:289 amelie/members/forms.py:378 @@ -4111,7 +3957,7 @@ msgstr "verwijderde activiteit" #: amelie/calendar/models.py:136 #, python-brace-format msgid "Payment for enrollment to {event} on {date}" -msgstr "" +msgstr "Betaling voor inschrijving voor {event} op {date}" #: amelie/calendar/models.py:170 amelie/education/models.py:134 #: amelie/room_duty/models.py:40 amelie/room_duty/models.py:109 @@ -7437,10 +7283,9 @@ msgstr "" "en %(error_count)s niet geaccepteerd." #: amelie/members/ajax_views.py:220 -#, fuzzy, python-brace-format -#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +#, python-brace-format msgid "Early termination of membership {membership_type} ({begin_year}/{end_year})" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" +msgstr "Voortijdige beëindiging van lidmaatschap {membership_type} ({begin_year}/{end_year})" #: amelie/members/forms.py:36 amelie/members/models.py:324 #: amelie/members/models.py:1401 @@ -7704,7 +7549,7 @@ msgstr "Kon BIC niet automatisch bepalen, vul deze alsjeblieft zelf in." #: amelie/members/forms.py:687 amelie/members/forms.py:720 msgid "The bank associated with your BIC does not accept SEPA Direct Debits." -msgstr "" +msgstr "De bank die bij de ingevulde BIC hoort accepteert geen SEPA-incasso's." #: amelie/members/forms.py:690 msgid "The BIC country does not match the IBAN country." @@ -7745,22 +7590,19 @@ msgid "Contribution {membership_type} ({begin_year}/{end_year})" msgstr "Contributie {membership_type} ({begin_year}/{end_year})" #: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:202 -#, fuzzy, python-brace-format -#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +#, python-brace-format msgid "Internal settlement for reversed contribution {membership_type} ({begin_year}/{end_year}) (migrated)" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" +msgstr "Interne verrekening voor teruggeboekte contributie {membership_type} ({begin_year}/{end_year}) (gemigreerd)" #: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:234 -#, fuzzy, python-brace-format -#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +#, python-brace-format msgid "Manual payment for contribution {membership_type} ({begin_year}/{end_year}) (migrated)" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" +msgstr "Handmatige betaling voor contributie {membership_type} ({begin_year}/{end_year}) (gemigreerd)" #: amelie/members/migrations/0024_delete_payment_delete_paymenttype.py:266 -#, fuzzy, python-brace-format -#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +#, python-brace-format msgid "Pre-SEPA payment for contribution {membership_type} ({begin_year}/{end_year}) (migrated)" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" +msgstr "Pre-SEPA-betaling voor contributie {membership_type} ({begin_year}/{end_year}) (gemigreerd)" #: amelie/members/models.py:47 amelie/members/models.py:68 #: amelie/members/models.py:93 amelie/members/models.py:1197 @@ -8147,10 +7989,8 @@ msgid "people" msgstr "personen" #: amelie/members/models.py:628 -#, fuzzy -#| msgid "Add transaction" msgid "Invalid transaction type" -msgstr "Transactie toevoegen" +msgstr "Ongeldige transactiesoort" #: amelie/members/models.py:750 msgid "Your email address is not to point to an Inter-Actief server. If you have a Google Apps account please use firstname.lastname@gapps.inter-actief.nl as an alternative." @@ -8259,10 +8099,9 @@ msgid "memberships" msgstr "lidmaatschappen" #: amelie/members/models.py:1100 -#, fuzzy, python-brace-format -#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +#, python-brace-format msgid "Payment for contribution {membership_type} ({begin_year}/{end_year})" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" +msgstr "Betaling voor contributie {membership_type} ({begin_year}/{end_year})" #: amelie/members/models.py:1153 amelie/members/models.py:1589 #: amelie/personal_tab/models.py:672 @@ -9146,10 +8985,8 @@ msgid "Your IBAN bank account number. For example \"NL18ABNA0484869868\". msgstr "Je IBAN-bankrekeningnummer. Bijvoorbeeld \"NL18ABNA0484869868\"." #: amelie/members/templates/includes/registration/authorizations.html:71 -#, fuzzy -#| msgid "Your BIC-code. Not required for Dutch bank accounts. For example \"ABNANL2A\". Find your BIC by clicking here (opens in new tab)." msgid "Your BIC-code. Not required for Dutch bank accounts. For example \"ABNANL2A\". Find your BIC by clicking here (opens in new tab)." -msgstr "Je BIC-code. Niet vereist voor Nederlandse rekeningnummers. Bijvoorbeeld \"ABNANL2A\". Vind je BIC door hier te klikken (opent in nieuwe tab)." +msgstr "Je BIC-code. Niet vereist voor Nederlandse rekeningnummers. Bijvoorbeeld \"ABNANL2A\". Vind je BIC door hier te klikken (opent nieuw tabblad)." #: amelie/members/templates/includes/registration/contact_member.html:6 msgid "Where can we reach you?" @@ -10142,7 +9979,7 @@ msgstr "Terug naar deze persoon." #: amelie/members/templates/person_confirm_mandate.html:3 msgid "Confirm BIC for mandate" -msgstr "" +msgstr "Bevestig BIC voor machtiging" #: amelie/members/templates/person_confirm_mandate.html:12 #, python-format @@ -10152,6 +9989,10 @@ msgid "" " Are you sure you want to add a mandate with the following IBAN: %(iban)s and BIC: %(bic)s?\n" " " msgstr "" +"\n" +" De BIC van deze machtiging staat op de lijst van bij ons bekende BICs van banken die geen SEPA Incasso's ondersteunen.\n" +"Weet je zeker dat je een machtiging met het volgende IBAN: %(iban)s en BIC: %(bic)s wilt toevoegen?\n" +" " #: amelie/members/templates/person_confirm_mandate.html:17 msgid "" @@ -10159,6 +10000,9 @@ msgid "" " Only do this if you are sure that this bank accepts SEPA Direct Debits.\n" " " msgstr "" +"\n" +" Doe dit alleen als je zeker weet dat deze bank SEPA Direct Debits ondersteunt.\n" +" " #: amelie/members/templates/person_confirm_mandate.html:26 #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:57 @@ -10613,7 +10457,7 @@ msgstr "Collegejaar" #: amelie/members/templates/person_membership.html:47 msgid "Paid?" -msgstr "" +msgstr "Betaald?" #: amelie/members/templates/person_membership.html:63 #, python-format @@ -10622,11 +10466,11 @@ msgstr "Beëindigd per %(ended)s" #: amelie/members/templates/person_membership.html:72 msgid "N/A" -msgstr "" +msgstr "n.v.t." #: amelie/members/templates/person_membership.html:75 msgid "For some reason this free membership has payments! This should not happen." -msgstr "" +msgstr "Om de een of andere reden heeft dit gratis lidmaatschap betalingen! Dit zou niet mogen gebeuren." #: amelie/members/templates/person_membership.html:143 msgid "Download PDF form" @@ -10649,38 +10493,30 @@ msgid "Terminate membership" msgstr "Einde lidmaatschap" #: amelie/members/templates/person_membership_create_payment.html:5 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Do you want to add %(member)s to %(group)s?\n" -#| " " +#, python-format msgid "" "\n" " Create manual payment for %(membership)s of %(person)s\n" " " msgstr "" "\n" -" Wil je %(member)s toevoegen aan %(group)s?\n" -" " +" Handmatige betaling aanmaken voor %(membership)s van %(person)s\n" +" " #: amelie/members/templates/person_membership_payments.html:20 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Do you want to remove %(member)s from %(group)s?\n" -#| " " +#, python-format msgid "" "\n" " Payments for %(membership)s %(year_1)s/%(year_2)s\n" " " msgstr "" "\n" -" Wil je %(member)s verwijderen uit %(group)s?\n" -" " +" Betalingen voor %(membership)s %(year_1)s/%(year_2)s\n" +" " #: amelie/members/templates/person_membership_payments.html:62 msgid "No transactions found. If this is a paid membership, this is probably wrong! Please contact the WWW." -msgstr "" +msgstr "Geen transacties gevonden. Als dit een betaald lidmaatschap is dan klopt dit waarschijnlijk niet! Neem dan contact op met de WWW." #: amelie/members/templates/person_membership_retrieve_signed.html:6 msgid "The following document was successfully retrieved from Documenso and saved to the membership:" @@ -10963,19 +10799,15 @@ msgid "If the member has signed their membership and authorization forms, the me msgstr "Als het lid hun lidmaatschaps- en machtigingsformulieren heeft ondertekend, dan zal het lidmaatschap en de eventuele machtigingen automatisch worden geactiveerd, en zal het lid verdwijnen uit deze lijst." #: amelie/members/templates/preregistration_status.html:30 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " With this form, you can edit the amendment to the %(authorization)s authorization.\n" -#| " " +#, python-format msgid "" "\n" " To view already activated memberships from this academic year, check the current enrollment status instead.\n" " " msgstr "" "\n" -" Met dit formulier kun je een amendement aanpassen voor machtiging %(authorization)s.\n" -" " +" Om lidmaatschappen uit dit academisch jaar te bekijken die al geactiveerd zijn, bekijk je de huidige inschrijvingsstatus.\n" +" " #: amelie/members/templates/preregistration_status.html:35 msgid "You can (re-)print the forms for that member by clicking the \"Print\" () button." @@ -11008,7 +10840,7 @@ msgstr "%(len)s inschrijving(en)" #: amelie/members/templates/preregistration_status.html:81 msgid "Print membership forms (opens in new tab)" -msgstr "Lidmaatschapsformulieren printen (opent in nieuwe tab)" +msgstr "Lidmaatschapsformulieren printen (opent nieuw tabblad)" #: amelie/members/templates/preregistration_status.html:88 msgid "A signature request for this membership is in progress. Click to open the request in Documenso" @@ -11032,10 +10864,8 @@ msgid "No pre-enrollments" msgstr "Geen voorinschrijvingen" #: amelie/members/templates/preregistration_status.html:114 -#, fuzzy -#| msgid "There are currently no pre-enrollments." msgid "There are currently no pending pre-enrollments." -msgstr "Er zijn momenteel geen voorinschrijvingen." +msgstr "Er zijn momenteel geen openstaande voorinschrijvingen." #: amelie/members/templates/query.html:5 amelie/members/templates/query.html:63 msgid "Search for members" @@ -11110,47 +10940,36 @@ msgstr "Er zijn geen rfid-kaarten geregistreerd." #: amelie/members/templates/registration_status.html:5 #: amelie/members/templates/registration_status.html:11 -#, fuzzy -#| msgid "enrollment start" msgid "Enrollment status" -msgstr "inschrijving start" +msgstr "inschrijvingsstatus" #: amelie/members/templates/registration_status.html:14 msgid "These are the new members this association year, per do-group. These memberships are already activated." -msgstr "" +msgstr "Dit zijn de nieuwe leden in dit verenigingsjaar, per doegroep. Deze lidmaatschappen zijn al geactiveerd." #: amelie/members/templates/registration_status.html:18 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " For this activity, there are %(places)s places left.\n" -#| " " +#, python-format msgid "" "\n" " To view unactivated memberships, check the current pre-enrollment status instead.\n" " " msgstr "" "\n" -" Voor deze activiteit zijn er nog %(places)s plaatsen beschikbaar.\n" +" Om lidmaatschappen te bekijken die nog niet zijn geactiveerd, bekijk je de huidige voorinschrijvingsstatus.\n" " " #: amelie/members/templates/registration_status.html:31 -#, fuzzy, python-format -#| msgid "%(len)s enrollment(s)" +#, python-format msgid "%(len)s member(s)" -msgstr "%(len)s inschrijving(en)" +msgstr "%(len)s lid/leden" #: amelie/members/templates/registration_status.html:50 -#, fuzzy -#| msgid "No pre-enrollments" msgid "No enrollments" -msgstr "Geen voorinschrijvingen" +msgstr "Geen inschrijvingen" #: amelie/members/templates/registration_status.html:53 -#, fuzzy -#| msgid "There is no enrollment start date entered." msgid "There have not been any enrollments this year yet." -msgstr "Er is geen inschrijfstartdatum opgegeven." +msgstr "Er zijn nog geen inschrijvingen geweest in dit jaar." #: amelie/members/templates/statistics/overview.html:5 msgid "Member statistics" @@ -11684,20 +11503,16 @@ msgid "Short description of the reason for the amendment" msgstr "Korte omschrijving reden van amendement" #: amelie/personal_tab/forms.py:131 -#, fuzzy -#| msgid "Payment" msgid "Payment date" -msgstr "Betaling" +msgstr "Betaaldatum" #: amelie/personal_tab/forms.py:134 -#, fuzzy -#| msgid "amount" msgid "Paid amount" -msgstr "bedrag" +msgstr "Betaald bedrag" #: amelie/personal_tab/forms.py:135 msgid "The amount that was paid in this settlement. Can be different from the amount that needs to be paid, can even be negative (if money was paid out by the association). Useful if, say, the transactions add up to 9 euro, you can enter a payment of 10 euro, which leaves 1 euro extra as balance on the person's personal tab." -msgstr "" +msgstr "Het bedrag dat is betaald in deze schikking. Kan anders zijn dan het te betalen bedrag, kan zelfs negatief zijn (als de vereniging geld heeft betaald). Handig als, zeg, de transacties totaal 9 euro zijn, kan je een betaling van 10 euro invoeren zodat er 1 euro extra saldo op de streeplijst van de persoon terecht komt." #: amelie/personal_tab/forms.py:166 msgid "Not signed" @@ -11837,20 +11652,20 @@ msgid "Select documents to attach to the declaration." msgstr "Selecteer documenten om toe te voegen aan de declaratie." #: amelie/personal_tab/forms.py:485 -#, fuzzy -#| msgid "Is Inter-Actief" msgid "Inter-Actief" -msgstr "Is Inter-Actief" +msgstr "Inter-Actief" #: amelie/personal_tab/forms.py:487 msgid "Bookkeeping" -msgstr "" +msgstr "Boekhouding" #: amelie/personal_tab/forms.py:488 msgid "" "Select the bookkeeping where the declaration should be sent.\n" "If you do not know what this means, do not change it." msgstr "" +"Kies de boekhouding waar de declaratie naartoe gestuurd moet worden.\n" +"Als je niet weet wat dit betekent, verander het dan niet." #: amelie/personal_tab/forms.py:505 msgid "-- Other --" @@ -11889,7 +11704,7 @@ msgstr "Vul een omschrijving in voor je declaratie." #: amelie/personal_tab/migrations/0015_personaltabsettlement_and_more.py:175 #, python-brace-format msgid "Settlement for transactions handled before SEPA debits were introduced - {person_name}" -msgstr "" +msgstr "Schikking voor transacties afgehandeld voor introductie SEPA-machtigingen - {person_name}" #: amelie/personal_tab/models.py:34 amelie/personal_tab/models.py:752 msgid "begin" @@ -11959,17 +11774,15 @@ msgstr "toegevoegd door" #: amelie/personal_tab/models.py:109 msgid "Discount balance" -msgstr "Kortingstegoeden" +msgstr "Kortingstegoed" #: amelie/personal_tab/models.py:110 msgid "discount balances" msgstr "kortingstegoeden" #: amelie/personal_tab/models.py:142 -#, fuzzy -#| msgid "pre-settlement" msgid "Settlement" -msgstr "pre-settlement" +msgstr "Schikking" #: amelie/personal_tab/models.py:152 #: amelie/personal_tab/templates/print/refund.html:48 @@ -11992,58 +11805,40 @@ msgid "Transactions" msgstr "Transacties" #: amelie/personal_tab/models.py:164 amelie/personal_tab/views.py:1008 -#, fuzzy -#| msgid "No activity transactions." msgid "Activity Transaction" -msgstr "Geen activiteittransacties." +msgstr "Activiteittransactie" #: amelie/personal_tab/models.py:166 amelie/personal_tab/views.py:1009 -#, fuzzy -#| msgid "Alexia transactions" msgid "Alexia Transaction" -msgstr "Alexiatransacties" +msgstr "Alexiatransactie" #: amelie/personal_tab/models.py:168 amelie/personal_tab/views.py:1010 -#, fuzzy -#| msgid "Contribution transactions" msgid "Contribution Transaction" -msgstr "Contributietransacties" +msgstr "Contributietransactie" #: amelie/personal_tab/models.py:170 amelie/personal_tab/views.py:1007 -#, fuzzy -#| msgid "Cookie Corner Login" msgid "Cookie Corner Transaction" -msgstr "Cookie Corner Login" +msgstr "Cookie Corner transactie" #: amelie/personal_tab/models.py:172 amelie/personal_tab/views.py:1011 -#, fuzzy -#| msgid "Custom transactions" msgid "Custom Transaction" -msgstr "Overige transacties" +msgstr "Overige transactie" #: amelie/personal_tab/models.py:174 amelie/personal_tab/views.py:1015 -#, fuzzy -#| msgid "Delete transaction" msgid "Debt Collection Transaction" -msgstr "Verwijder transactie" +msgstr "Incassotransactie" #: amelie/personal_tab/models.py:176 amelie/personal_tab/views.py:1016 -#, fuzzy -#| msgid "Transaction" msgid "Reversal Transaction" -msgstr "Transactie" +msgstr "Terugboekingstransactie" #: amelie/personal_tab/models.py:178 amelie/personal_tab/views.py:1018 -#, fuzzy -#| msgid "Balance transaction" msgid "Extra Balance Transaction" -msgstr "Tegoedtransacties" +msgstr "Extra saldotransactie" #: amelie/personal_tab/models.py:180 amelie/personal_tab/views.py:1017 -#, fuzzy -#| msgid "Balance transaction" msgid "Manual Payment Transaction" -msgstr "Tegoedtransacties" +msgstr "Handmatige betalingstransactie" #: amelie/personal_tab/models.py:228 #: amelie/personal_tab/templates/lists/activity_transactions.html:14 @@ -12337,16 +12132,12 @@ msgid "direct withdrawal-batches" msgstr "incasso-batches" #: amelie/personal_tab/models.py:892 -#, fuzzy -#| msgid "Personal tab statistics" msgid "personal tab settlement" -msgstr "Streeplijststatistieken" +msgstr "Streeplijstschikking" #: amelie/personal_tab/models.py:893 -#, fuzzy -#| msgid "Personal tab statistics" msgid "personal tab settlements" -msgstr "Streeplijststatistieken" +msgstr "Streeplijstschikkingen" #: amelie/personal_tab/models.py:911 msgid "batch" @@ -12369,130 +12160,106 @@ msgid "direct withdrawal-instructions" msgstr "incasso-instructies" #: amelie/personal_tab/models.py:956 -#, fuzzy -#| msgid "Name (en)" msgid "Name (NL)" -msgstr "Naam (en)" +msgstr "Naam (NL)" #: amelie/personal_tab/models.py:957 -#, fuzzy -#| msgid "Name (en)" msgid "Name (EN)" -msgstr "Naam (en)" +msgstr "Naam (EN)" #: amelie/personal_tab/models.py:958 -#, fuzzy -#| msgid "Description (en)" msgid "Description (NL)" -msgstr "Omschrijving (en)" +msgstr "Omschrijving (NL)" #: amelie/personal_tab/models.py:959 -#, fuzzy -#| msgid "Description (en)" msgid "Description (EN)" -msgstr "Omschrijving (en)" +msgstr "Omschrijving (EN)" #: amelie/personal_tab/models.py:960 msgid "This payment method will be visible when creating a manual payment directly in someone's personal tab." -msgstr "" +msgstr "Deze betaalmethode zal zichtbaar zijn tijdens het maken van een handmatige betaling direct in iemand's streeplijst." #: amelie/personal_tab/models.py:961 -#, fuzzy -#| msgid "No membership" msgid "Visible for memberships" -msgstr "Geen lidmaatschap" +msgstr "Zichtbaar voor lidmaatschappen" #: amelie/personal_tab/models.py:961 msgid "This payment method will be visible when privileged members enter a manual payment for a membership." -msgstr "" +msgstr "Deze betaalmethode zal zichtbaar zijn als bevoegde leden een handmatige betaling voor een lidmaatschap invoeren." #: amelie/personal_tab/models.py:962 -#, fuzzy -#| msgid "iCal all activities" msgid "Visible for activities" -msgstr "iCal alle activiteiten" +msgstr "Zichtbaar voor activiteiten" #: amelie/personal_tab/models.py:962 msgid "This payment method will be visible when privileged members enter a manual payment for an activity." -msgstr "" +msgstr "Deze betaalmethode zal zichtbaar zijn als bevoegde leden een handmatige betaling voor een activiteit invoeren." #: amelie/personal_tab/models.py:963 msgid "Icon name for frontend" -msgstr "" +msgstr "Naam van icoon voor de frontend" #: amelie/personal_tab/models.py:967 -#, fuzzy -#| msgid "Payment Method" msgid "payment method" -msgstr "Betalingsmethode" +msgstr "betaalmethode" #: amelie/personal_tab/models.py:968 -#, fuzzy -#| msgid "Payment Method" msgid "payment methods" -msgstr "Betalingsmethode" +msgstr "betaalmethodes" #: amelie/personal_tab/models.py:995 msgid "Frontend Icon" -msgstr "" +msgstr "Icoon in frontend" #: amelie/personal_tab/models.py:1037 -#, fuzzy -#| msgid "payment" msgid "payment date" -msgstr "betaling" +msgstr "betaaldatum" #: amelie/personal_tab/models.py:1046 #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:58 -#, fuzzy -#| msgid "Created on" msgid "Created by" -msgstr "Aangemaakt op" +msgstr "Aangemaakt door" #: amelie/personal_tab/models.py:1057 -#, fuzzy -#| msgid "pre-settlement" msgid "manual payment settlement" -msgstr "pre-settlement" +msgstr "schikking voor handmatige betaling" #: amelie/personal_tab/models.py:1058 -#, fuzzy -#| msgid "pre-settlement" msgid "manual payment settlements" -msgstr "pre-settlement" +msgstr "schikkingen voor handmatige betaling" #: amelie/personal_tab/models.py:1092 msgid "Manual settlements can only be deleted in the same association year as they were paid in." -msgstr "" +msgstr "Handmatige schikkingen kunnen alleen worden verwijderd in hetzelfde verenigingsjaar waarin ze zijn betaald." #: amelie/personal_tab/models.py:1107 msgid "The extra balance that was created in this settlement was already used elsewhere." -msgstr "" +msgstr "Het extra saldo dat in deze schikking is aangemaakt is al ergens anders voor gebruikt." #: amelie/personal_tab/models.py:1112 #, python-brace-format msgid "The total sum of the transactions ({transactions_sum}) does not inversely equal the linked manual payment's amount ({manual_amount})." -msgstr "" +msgstr "De totaalsom van de transacties ({transactions_sum}) is niet omgekeerd hetzelfde als het bedrag van de gekoppelde handmatige betaling ({manual_amount})." #: amelie/personal_tab/models.py:1117 #, python-brace-format msgid "The settlement's amount ({settlement_amount}) does not inversely equal the manual payment's amount and the extra balance ({manual_amount} + {extra_balance} = {total})." -msgstr "" +msgstr "Het bedrag van de schikking ({settlement_amount}) is niet omgekeerd hetzelfde als het bedrag van de handmatige betaling en het extra saldo ({manual_amount} + {extra_balance} = {total})." #: amelie/personal_tab/models.py:1170 #, python-brace-format msgid "Deposit to personal tab balance on {date} for {name}" -msgstr "" +msgstr "Bijschrijving op streeplijstsaldo op {date} voor {name}" #: amelie/personal_tab/models.py:1174 #, python-brace-format msgid "Refund of personal tab balance on {date} for {name}" -msgstr "" +msgstr "Terugbetaling van streeplijstsaldo op {date} voor {name}" #: amelie/personal_tab/models.py:1178 #, python-brace-format msgid "Leftover balance after settlement on {date} for {name}" -msgstr "" +msgstr "Overgebleven saldo na schikking op {date} voor {name}" #: amelie/personal_tab/models.py:1268 msgid "instruction" @@ -12532,32 +12299,24 @@ msgid "reversal" msgstr "terugboeking" #: amelie/personal_tab/models.py:1330 -#, fuzzy -#| msgid "Date last used" msgid "Date added" -msgstr "Datum laatst gebruikt" +msgstr "Datum toegevoegd" #: amelie/personal_tab/models.py:1331 -#, fuzzy -#| msgid "Edit reversal" msgid "First reversal" -msgstr "Terugboeking wijzigen" +msgstr "Eerste terugboeking" #: amelie/personal_tab/models.py:1332 -#, fuzzy -#| msgid "Insert reversal" msgid "Last reversal" -msgstr "Terugboeking invoeren" +msgstr "Laatste terugboeking" #: amelie/personal_tab/models.py:1360 -#, fuzzy -#| msgid "unknown id" msgid "known bad BIC" -msgstr "unknown id" +msgstr "bekende foute BIC" #: amelie/personal_tab/models.py:1361 msgid "known bad BICs" -msgstr "" +msgstr "bekende foute BICs" #: amelie/personal_tab/models.py:1374 #: amelie/personal_tab/templates/print/log_list.html:30 @@ -12601,7 +12360,7 @@ msgstr "voor persoonlijk gebruik" #: amelie/personal_tab/models.py:1456 msgid "bookkeeping" -msgstr "" +msgstr "boekhouding" #: amelie/personal_tab/pos_models.py:21 msgid "Login token" @@ -12718,8 +12477,6 @@ msgstr "Er is een onbekende fout opgetreden, probeer het opnieuw!" #: amelie/personal_tab/pos_views.py:311 #: amelie/personal_tab/register_views.py:233 -#, fuzzy -#| msgid "The login code is invalid or expired, please try again!." msgid "The login code is invalid or expired, please try again!" msgstr "Deze logincode is ongeldig of verlopen, probeer het opnieuw!" @@ -12808,16 +12565,12 @@ msgid "Print by {person} was refunded successfully." msgstr "Print van {person} is succesvol terugbetaald." #: amelie/personal_tab/register_views.py:81 -#, fuzzy -#| msgid "This card is already registered." msgid "This card has already been registered." msgstr "Deze kaart is al geregistreerd." #: amelie/personal_tab/register_views.py:165 -#, fuzzy -#| msgid "Card is registered." msgid "Card successfully registered." -msgstr "Kaart is geregistreerd." +msgstr "Kaart succesvol geregistreerd." #: amelie/personal_tab/statistics.py:280 msgid "{} ledger statistics" @@ -12835,7 +12588,7 @@ msgstr "Streeplijststatistieken" #: amelie/personal_tab/templates/balance/balance_form.html:9 #: amelie/personal_tab/templates/statistics/statistics.html:96 msgid "Personal tab balance" -msgstr "Streeplijst totalen" +msgstr "Streeplijstsaldo" #: amelie/personal_tab/statistics.py:290 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:81 @@ -12981,10 +12734,9 @@ msgstr "Persoon/rekeninghouder" #: amelie/personal_tab/templates/print/log_list.html:56 #: amelie/personal_tab/templates/unpaid_memberships/forgive.html:29 #: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:49 -#, fuzzy, python-format -#| msgid "No transactions on personal tab." +#, python-format msgid "Go to %(name)s's personal tab" -msgstr "Geen streeplijsttransacties." +msgstr "Ga naar %(name)'s streeplijst." #: amelie/personal_tab/templates/authorization/list.html:108 #: amelie/personal_tab/templates/info/account_information.html:40 @@ -13150,15 +12902,15 @@ msgid "Cookie and candy list of %(person)s" msgstr "Streeplijst van %(person)s" #: amelie/personal_tab/templates/cookie_corner_dashboard.html:13 -#, fuzzy, python-format -#| msgid "Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s." +#, python-format msgid "" "\n" " Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s.\n" " " msgstr "" -"Je huidige streeplijstsaldo is € %(curr_balance)s,\n" -" met toekomstige transacties meegerekend is dat € %(all_balance)s." +"\n" +" Je huidige streeplijstsaldo is € %(curr_balance)s. Met toekomstige transacties meegerekend is dat € %(all_balance)s.\n" +" " #: amelie/personal_tab/templates/cookie_corner_dashboard.html:17 msgid "" @@ -13167,6 +12919,10 @@ msgid "" " If you do not have a mandate, you can settle any outstanding balance with a manual payment. To arrange such a payment, please contact the board.\n" " " msgstr "" +"\n" +" Als je een incassomachtiging hebt getekend, dan wordt het saldo normaliter verrekend door deze periodiek te incasseren van je bankrekening. \n" +"Als je geen machtiging hebt getekend, dan kan je het openstaande saldo handmatig betalen. Om een betaling te doen neem je contact op met het bestuur.\n" +" " #: amelie/personal_tab/templates/cookie_corner_dashboard.html:24 #, python-format @@ -13213,10 +12969,8 @@ msgstr "Eerdere transacties" #: amelie/personal_tab/templates/cookie_corner_dashboard.html:60 #: amelie/personal_tab/templates/cookie_corner_overview.html:30 #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:54 -#, fuzzy -#| msgid "Usage transactions" msgid "Unpaid transactions" -msgstr "Verbruiktransacties" +msgstr "Onbetaalde transacties" #: amelie/personal_tab/templates/cookie_corner_dashboard.html:63 msgid "History of exam credit" @@ -13229,10 +12983,8 @@ msgid "Cookie Corner Wrapped" msgstr "Cookie Corner Wrapped" #: amelie/personal_tab/templates/cookie_corner_dashboard.html:72 -#, fuzzy -#| msgid "Board tasks" msgid "Board tasks:" -msgstr "Bestuurstaken" +msgstr "Bestuurstaken:" #: amelie/personal_tab/templates/cookie_corner_dashboard.html:74 msgid "Add transaction to personal tab" @@ -13251,17 +13003,15 @@ msgstr "Tentamenkoekentegoed toevoegen" #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:4 #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:8 msgid "Manual Payment Settlement" -msgstr "" +msgstr "Schikking voor Handmatige Betaling" #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:14 msgid "Reference of manual payment" -msgstr "" +msgstr "Referentie van handmatige betaling" #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:37 -#, fuzzy -#| msgid "way of payment" msgid "Date of payment" -msgstr "betalingswijze" +msgstr "Betaaldatum" #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:41 msgid "Method of payment" @@ -13270,16 +13020,14 @@ msgstr "Betaalwijze" #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:79 #: amelie/personal_tab/templates/cookie_corner_manual_payment_settlement_view.html:84 #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:4 -#, fuzzy -#| msgid "Delete payment" msgid "Delete manual payment" -msgstr "Verwijder betaling" +msgstr "Verwijder handmatige betaling" #: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:4 #: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:8 #, python-format msgid "Manual payments - Page %(page)s/%(total)s" -msgstr "" +msgstr "Handmatige betalingen - Pagina %(page)s/%(total)s" #: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:12 msgid "" @@ -13289,6 +13037,11 @@ msgid "" " a negative amount to their personal tab to create a credit they can use for activities.\n" " " msgstr "" +"\n" +" Dit zijn betalingen die mensen handmatig hebben gedaan direct aan de vereniging, bijvoorbeeld contant of met PIN. \n" +"Een handmatige betaling kan betalen voor transacties die tegen de streeplijst van een persoon zijn gedaan, of het kan \n" +"een negatief bedrag toevoegen aan de streeplijst om zo een krediet te maken die gebruikt kan worden voor activiteiten.\n" +" " #: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:19 msgid "" @@ -13297,6 +13050,9 @@ msgid "" " and if there was any extra money deposited.\n" " " msgstr "" +"\n" +" Klik op een betalingsbeschrijving om details te bekijken over voor welke transacties de handmatige betaling heeft betaald, en of er extra geld is bijgeschreven.\n" +" " #: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:27 msgid "" @@ -13305,13 +13061,15 @@ msgid "" " and click on the \"Add manual payment\" button in the relevant block.\n" " " msgstr "" +"\n" +" Om een handmatige betaling aan te maken, ga je naar het streeplijstoverzicht van de persoon \n" +"en klik je op de knop \"Handmatige betaling toevoegen\" in het relevante blok.\n" +" " #: amelie/personal_tab/templates/cookie_corner_manual_payments_list.html:78 #: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:42 -#, fuzzy -#| msgid "No amendments have been found." msgid "No manual payments have been found." -msgstr "Er zijn geen amendementen gevonden." +msgstr "Er zijn geen handmatige betalingen gevonden." #: amelie/personal_tab/templates/cookie_corner_overview.html:13 msgid "Personal overview" @@ -13339,20 +13097,17 @@ msgstr "Incasso-opdrachten (SEPA)" #: amelie/personal_tab/templates/cookie_corner_overview.html:28 #: amelie/personal_tab/templates/info/payments.html:59 -#, fuzzy -#| msgid "payments" msgid "Manual payments" -msgstr "betalingen" +msgstr "Handmatige betalingen" #: amelie/personal_tab/templates/cookie_corner_overview.html:29 msgid "Unpaid memberships" msgstr "Onbetaalde lidmaatschappen" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:8 -#, fuzzy, python-format -#| msgid "Delete transaction '%(object)s'" +#, python-format msgid "Delete manual payment '%(object)s'" -msgstr "Verwijder transactie '%(object)s'" +msgstr "Verwijder handmatige betaling '%(object)s'" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_confirm_delete.html:11 #: amelie/personal_tab/templates/transactions/transaction_delete.html:11 @@ -13366,16 +13121,14 @@ msgstr "" "Weet je zeker dat je '%(object)s' van %(person)s wilt verwijderen?" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:4 -#, fuzzy, python-format -#| msgid "Direct debit for %(person)s" +#, python-format msgid "Create manual payment for %(person)s" -msgstr "Incasso's voor %(person)s" +msgstr "Handmatige betaling toevoegen voor %(person)s" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:9 -#, fuzzy, python-format -#| msgid "Direct debit for %(person)s" +#, python-format msgid "New manual payment for %(person)s" -msgstr "Incasso's voor %(person)s" +msgstr "Nieuwe handmatige betaling voor %(person)s" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:14 #: amelie/personal_tab/templates/transactions/transaction_form.html:20 @@ -13388,107 +13141,80 @@ msgid "You are now adding a transaction in the category \"Other\"" msgstr "Je voegt nu een overige transactie toe" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:32 -#, fuzzy -#| msgid "Total cost" msgid "Total of selected" -msgstr "Totaalbedrag" +msgstr "Totaal van geselecteerd" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:33 msgid "Amount being paid" -msgstr "" +msgstr "Bedrag dat wordt betaald" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:34 -#, fuzzy -#| msgid "Personal tab balance" msgid "Personal tab balance before payment" -msgstr "Streeplijst totalen" +msgstr "Streeplijstsaldo voor betaling" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:35 -#, fuzzy -#| msgid "Personal tab balance" msgid "Personal tab balance after payment" -msgstr "Streeplijst totalen" +msgstr "Streeplijstsaldo na betaling" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:38 #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:40 -#, fuzzy -#| msgid "Today:" msgid "Today" -msgstr "Vandaag:" +msgstr "Vandaag" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:39 #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:41 msgid "Including future" -msgstr "" +msgstr "Inclusief toekomstig" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:56 -#, fuzzy -#| msgid "" -#| "\n" -#| " One other person has filled in their availability.\n" -#| " " -#| msgid_plural "" -#| "\n" -#| " %(count)s others have filled in their availability.\n" -#| " " msgid "" "\n" " Please select the transactions that will be marked as paid with this settlement.\n" " " msgstr "" "\n" -"Een persoon heeft al zijn beschikbaarheid aangegeven." +" Selecteer a.u.b. de transacties die als betaald zullen worden gemarkeerd met deze schikking.\n" +" " #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:59 -#, fuzzy -#| msgid "" -#| "\n" -#| " You are not enrolled in this activity yet.\n" -#| " " msgid "" "\n" " Rows marked in green are transactions that have a date in the future.\n" " " msgstr "" "\n" -"Je doet nog niet mee met deze activiteit." +" Rijen gemarkeerd met groen zijn transacties met een datum in de toekomst.\n" +" " #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:66 msgid "There were errors while submitting the form." -msgstr "" +msgstr "Er zijn fouten gevonden tijdens het opsturen van het formulier." #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:76 -#, fuzzy -#| msgid "For more information, see:" msgid "For more errors, see below." -msgstr "Zie voor meer informatie" +msgstr "Zie hieronder voor meer fouten." #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:79 msgid "See below for the errors." -msgstr "" +msgstr "Zie de fouten hieronder" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:87 msgid "Include?" -msgstr "" +msgstr "Meenemen?" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:90 -#, fuzzy -#| msgid "ID" msgid "#ID" -msgstr "ID" +msgstr "#ID" #: amelie/personal_tab/templates/cookie_corner_person_manual_payment_create.html:121 -#, fuzzy -#| msgid "No transactions found" msgid "No unpaid transactions found!" -msgstr "Geen transacties gevonden" +msgstr "Geen onbetaalde transacties gevonden!" #: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:4 #: amelie/personal_tab/templates/cookie_corner_person_manual_payments.html:8 -#, fuzzy, python-format -#| msgid "Direct debit for %(person)s" +#, python-format msgid "Manual payments for %(person)s" -msgstr "Incasso's voor %(person)s" +msgstr "Handmatige betalingen voor %(person)s" #: amelie/personal_tab/templates/cookie_corner_price_list.html:22 #: amelie/personal_tab/templates/cookie_corner_price_list.html:65 @@ -13539,10 +13265,8 @@ msgstr "Alexia" #: amelie/personal_tab/templates/cookie_corner_transactions.html:48 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:70 #: amelie/personal_tab/templates/lists/transactions_totals.html:25 -#, fuzzy -#| msgid "instruction" msgid "Contribution" -msgstr "instructie" +msgstr "Contributie" #: amelie/personal_tab/templates/cookie_corner_transactions.html:54 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:45 @@ -13553,48 +13277,39 @@ msgid "Payments" msgstr "Betalingen" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:5 -#, fuzzy -#| msgid "Transactions" msgid "Unpaid Transactions" -msgstr "Transacties" +msgstr "Onbetaalde transacties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:9 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " No transactions found\n" -#| " " +#, python-format msgid "" "\n" " Unpaid Transactions for %(name)s\n" " " msgstr "" "\n" -"Geen transacties gevonden" +" Onbetaalde Transacties voor %(name)s\n" +" " #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:15 -#, fuzzy, python-format -#| msgid "Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s." +#, python-format msgid "" "\n" " Your current balance is € %(curr_balance)s. Including future transactions that will be € %(all_balance)s.\n" " " msgstr "" -"Je huidige streeplijstsaldo is € %(curr_balance)s,\n" -" met toekomstige transacties meegerekend is dat € %(all_balance)s." +"\n" +" Je huidige streeplijstsaldo is € %(curr_balance)s. Met toekomstige transacties meegerekend is dat € %(all_balance)s.\n" +" " #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:18 -#, fuzzy -#| msgid "Blame overview" msgid "Back to overview" -msgstr "Blame-overzicht" +msgstr "Terug naar overzicht" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:26 #: amelie/personal_tab/templates/info/transaction_information.html:3 -#, fuzzy -#| msgid "Transaction" msgid "Transaction type" -msgstr "Transactie" +msgstr "Transactiesoort" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:39 #: amelie/personal_tab/templates/lists/transactions_categorized.html:30 @@ -13603,207 +13318,157 @@ msgid "Subtotal" msgstr "Subtotaal" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:57 -#, fuzzy -#| msgid "Transactions" msgid "Transactions per type" -msgstr "Transacties" +msgstr "Transacties per soort" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions.html:59 -#, fuzzy -#| msgid "No transactions have been found" msgid "No unpaid transactions have been found" -msgstr "Er zijn geen transacties gevonden." +msgstr "Er zijn geen onbetaalde transacties gevonden" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:6 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:10 #, python-format msgid "Unpaid transactions - Page %(page)s/%(total)s" -msgstr "" +msgstr "Onbetaalde transacties - Pagina %(page)s/%(total)s" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:12 -#, fuzzy -#| msgid "There are people whose mandate has ended." msgid "These are people with transactions that have not been paid for." -msgstr "Er zijn personen met een beëindigde machtiging." +msgstr "Dit zijn mensen met transacties die nog niet betaald zijn." #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:16 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Your membership type is %(type)s.\n" -#| " " +#, python-format msgid "" "\n" " Only people without a mandate of type '%(filtered_type)s' are shown.\n" " " msgstr "" "\n" -"Je lidmaatschapstype is %(type)s." +" Alleen mensen zonder een machtiging van soort '%(filtered_type)s' worden getoond.\n" +" " #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:20 -#, fuzzy -#| msgid "Only activities that you are allowed to edit are shown." msgid "Only people without a mandate are shown." -msgstr "Alleen activiteiten die jij mag aanpassen worden getoond." +msgstr "Alleen mensen zonder machtiging worden getoond." #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:26 -#, fuzzy -#| msgid "" -#| "\n" -#| " The following people have uploaded a new profile picture.\n" -#| " " msgid "" "\n" " Only people with a mandate of type 'filtered_type' are shown.\n" " " msgstr "" "\n" -" De volgende personen hebben een nieuwe profielfoto geupload.\n" +" Alleen mensen met een machtiging van soort 'filtered_type' worden getoond.\n" " " #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:30 -#, fuzzy -#| msgid "Only activities in the future are shown." msgid "Only people with a mandate are shown." -msgstr "Alleen activiteiten in de toekomst worden getoond." +msgstr "Alleen mensen met een machtiging worden getoond." #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:35 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Last %(amount)s added transactions.\n" -#| " " +#, python-format msgid "" "\n" " Only transactions of type '%(filter_transactions)s' are shown.\n" " " msgstr "" "\n" -"Laatste %(amount)s toegevoegde transacties." +" Alleen transacties van soort '%(filter_transactions)s' worden getoond.\n" +" " #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:40 msgid "Each person can be expanded to view details about the individual transactions." -msgstr "" +msgstr "Elke persoon kan worden uitgeklapt om details over de individuele transacties te bekijken." #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:44 msgid "Total number of people" -msgstr "" +msgstr "Totaal aantal personen" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:48 -#, fuzzy -#| msgid "Number of transactions" msgid "Total number of transactions" -msgstr "Aantal transacties" +msgstr "Totaal aantal transacties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:52 -#, fuzzy -#| msgid "Total costs" msgid "Total sum of unpaid costs" -msgstr "Totaal bedrag" +msgstr "Totaalbedrag onbetaalde kosten" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:57 -#, fuzzy -#| msgid "Sister associations" msgid "Filter options" -msgstr "Zusterverenigingen" +msgstr "Filteropties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:60 -#, fuzzy -#| msgid "Enroll people" msgid "Only people with:" -msgstr "Personen inschrijven" +msgstr "Alleen mensen met:" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:62 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:70 -#, fuzzy -#| msgid "Contribution transactions" msgid "Contribution mandate" -msgstr "Contributietransacties" +msgstr "Contributiemachtiging" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:63 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:71 -#, fuzzy -#| msgid "Existing mandate" msgid "Consumption mandate" -msgstr "Bestaande machtiging" +msgstr "Consumptiemachtiging" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:64 #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:72 -#, fuzzy -#| msgid "End mandate" msgid "Any mandate" -msgstr "Machtiging beëindigen" +msgstr "Elk soort machtiging" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:68 msgid "Only people without:" -msgstr "" +msgstr "Alleen mensen zonder:" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:76 -#, fuzzy -#| msgid "Last transaction of the year: " msgid "Only transactions of type:" -msgstr "Laatste transactie van het jaar: " +msgstr "Alleen transacties van soort:" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:79 -#, fuzzy -#| msgid "No activity transactions." msgid "Activity transactions" -msgstr "Geen activiteittransacties." +msgstr "Activiteittransacties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:80 -#, fuzzy -#| msgid "No other transactions." msgid "Cookie corner transactions" -msgstr "Geen overige transacties." +msgstr "Cookie corner transacties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:83 -#, fuzzy -#| msgid "Usage transactions" msgid "Reversal transactions" -msgstr "Verbruiktransacties" +msgstr "Terugboekingstransacties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:88 -#, fuzzy -#| msgid " files." msgid "Clear filters" -msgstr " bestanden." +msgstr "Filters weghalen" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:91 msgid "Toggle expand/collapse all" -msgstr "" +msgstr "Alles uit-/samenvouwen" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:103 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Last %(count)s transactions\n" -#| " " +#, python-format msgid "" "\n" " %(name)s - (%(num_transactions)s) - € %(total_cost)s\n" " " msgstr "" "\n" -"Laatste %(count)s toegevoegde transacties." +" %(name)s - (%(num_transactions)s) - € %(total_cost)s\n" +" " #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:112 #, python-format msgid "Go to %(name)s's personal tab (opens new tab)" -msgstr "" +msgstr "Ga naar %(name)s's streeplijst (opent nieuw tabblad)" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:115 msgid "Create settlement (opens new tab)" -msgstr "" +msgstr "Schikking toevoegen (opent nieuw tabblad)" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:155 -#, fuzzy -#| msgid "No transactions" msgid "No unpaid transactions" -msgstr "Geen transacties" +msgstr "Geen onbetaalde transacties" #: amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html:157 msgid "There are no people with unpaid transactions! Woah! 🎉🎉🎉" -msgstr "" +msgstr "Er zijn geen mensen met onbetaalde transacties! Wow! 🎉🎉🎉" #: amelie/personal_tab/templates/debt_collection/instruction_reversal.html:4 #: amelie/personal_tab/templates/debt_collection/instruction_reversal.html:8 @@ -14058,10 +13723,9 @@ msgid "No batches have been found." msgstr "Er zijn geen batches gevonden." #: amelie/personal_tab/templates/declaration/declaration.mail:3 -#, fuzzy, python-format -#| msgid "Declaration submitted via Amélie" +#, python-format msgid "Declaration submitted via Amélie by %(name)s on %(date)s" -msgstr "Declaratie verstuurd via Amélie" +msgstr "Declaratie verstuurd via Amélie door %(name)s op %(date)s" #: amelie/personal_tab/templates/declaration/declaration.mail:24 msgid " I.C.T.S.V. Inter-Actief" @@ -14215,7 +13879,7 @@ msgstr "" #: amelie/personal_tab/templates/exam_cookie_credit/overview.html:30 msgid "Balance transaction" -msgstr "Tegoedtransacties" +msgstr "Saldotransactie" #: amelie/personal_tab/templates/exam_cookie_credit/overview.html:33 msgid "Usage transactions" @@ -14244,11 +13908,6 @@ msgid "Direct debits" msgstr "Incasso's" #: amelie/personal_tab/templates/info/payments.html:11 -#, fuzzy -#| msgid "" -#| "\n" -#| " Last %(amount)s executed or planned direct debits.\n" -#| " " msgid "" "\n" " Your most recently executed or planned direct debit.\n" @@ -14259,10 +13918,12 @@ msgid_plural "" " " msgstr[0] "" "\n" -"Laatste %(amount)s uitgevoerde of geplande incasso's." +" Je meest recent uitgevoerde of geplande incasso.\n" +" " msgstr[1] "" "\n" -"Laatste %(amount)s uitgevoerde of geplande incasso's." +" Je meest recent uitgevoerde of geplande incasso's.\n" +" " #: amelie/personal_tab/templates/info/payments.html:42 msgid "There are no direct debits executed or planned yet." @@ -14273,17 +13934,10 @@ msgid "View all direct debits." msgstr "Alle incasso's bekijken" #: amelie/personal_tab/templates/info/payments.html:62 -#, fuzzy -#| msgid "Add amendment" msgid "Add manual payment" -msgstr "Amendement invoeren" +msgstr "Handmatige betaling toevoegen" #: amelie/personal_tab/templates/info/payments.html:67 -#, fuzzy -#| msgid "" -#| "\n" -#| " Your membership type is %(type)s.\n" -#| " " msgid "" "\n" " Your most recent manual payment.\n" @@ -14294,20 +13948,20 @@ msgid_plural "" " " msgstr[0] "" "\n" -"Je lidmaatschapstype is %(type)s." +" Je meest recente handmatige betaling.\n" +" " msgstr[1] "" "\n" -"Je lidmaatschapstype is %(type)s." +" Je meest recente handmatige betalingen.\n" +" " #: amelie/personal_tab/templates/info/payments.html:100 -#, fuzzy -#| msgid "There are no contacts yet." msgid "There are no manual payments yet." -msgstr "Er zijn nog geen contacten." +msgstr "Er zijn nog geen handmatige betalingen." #: amelie/personal_tab/templates/info/payments.html:110 msgid "View all manual payments." -msgstr "" +msgstr "Alle handmatige betalingen laten zien." #: amelie/personal_tab/templates/info/rfid_cards.html:13 msgid "Type of card" @@ -14350,68 +14004,53 @@ msgid "Reversed collection" msgstr "Teruggeboekte incasso" #: amelie/personal_tab/templates/info/transaction_information.html:122 -#, fuzzy -#| msgid "Original" msgid "Originated in" -msgstr "Origineel" +msgstr "Ontstaan in" #: amelie/personal_tab/templates/info/transaction_settlement_info.html:3 -#, fuzzy -#| msgid "Print membership forms (opens in new tab)" msgid "Show settlement details (opens in new tab)" -msgstr "Lidmaatschapsformulieren printen (opent in nieuwe tab)" +msgstr "Schikkingsdetails laten zien (opent nieuw tabblad)" #: amelie/personal_tab/templates/info/transaction_settlement_info.html:9 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Add credit for %(person)s.\n" -#| " " +#, python-format msgid "" "\n" " Direct debit on %(date)s\n" " " msgstr "" "\n" -"Tentamenkoekentegoed toevoegen voor %(person)s" +" Incasso op %(date)s\n" +" " #: amelie/personal_tab/templates/info/transaction_settlement_info.html:17 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Enroll person for %(name)s\n" -#| " " +#, python-format msgid "" "\n" " Reversal on %(date)s\n" " " msgstr "" "\n" -"Persoon inschrijven voor %(name)s" +" Terugboeking op %(date)s\n" +" " #: amelie/personal_tab/templates/info/transaction_settlement_info.html:24 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Enroll person for %(name)s\n" -#| " " +#, python-format msgid "" "\n" " Manual payment on %(date)s\n" " " msgstr "" "\n" -"Persoon inschrijven voor %(name)s" +" Handmatige betaling op %(date)s\n" +" " #: amelie/personal_tab/templates/info/transaction_settlement_info.html:35 -#, fuzzy -#| msgid "No payment" msgid "UNKNOWN payment!" -msgstr "Geen betaling" +msgstr "ONBEKENDE betaling!" #: amelie/personal_tab/templates/info/transaction_settlement_info.html:40 msgid "Unpaid!" -msgstr "" +msgstr "Niet betaald!" #: amelie/personal_tab/templates/lists/activity_transactions.html:10 msgid "Added" @@ -14443,10 +14082,8 @@ msgid "No mandates have been found." msgstr "Er zijn geen machtigingen gevonden." #: amelie/personal_tab/templates/lists/contribution_transactions.html:58 -#, fuzzy -#| msgid "Contribution transactions" msgid "No contribution transactions." -msgstr "Contributietransacties" +msgstr "Geen contributietransacties." #: amelie/personal_tab/templates/lists/cookie_corner_transactions.html:65 msgid "No transactions on personal tab." @@ -14499,60 +14136,49 @@ msgid "No transactions" msgstr "Geen transacties" #: amelie/personal_tab/templates/lists/payment_transactions.html:44 -#, fuzzy -#| msgid "Direct debits" msgid "Direct debit" -msgstr "Incasso's" +msgstr "Incasso" #: amelie/personal_tab/templates/lists/payment_transactions.html:46 -#, fuzzy -#| msgid "Reversal of direct withdrawal {date}" msgid "Reversal of direct debit" -msgstr "Terugboeking incasso {date}" +msgstr "Terugboeking incasso" #: amelie/personal_tab/templates/lists/payment_transactions.html:78 -#, fuzzy -#| msgid "No payment" msgid "No payments." -msgstr "Geen betaling" +msgstr "Geen betalingen." #: amelie/personal_tab/templates/lists/transactions_totals.html:51 msgid "This date is in the future" -msgstr "" +msgstr "Deze datum is in de toekomst" #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:4 msgid "[Inter-Actief] Reminder of open balance on your personal tab" -msgstr "" +msgstr "[Inter-Actief] Herinnering openstaand streeplijstsaldo" #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:8 msgid "This e-mail serves as a reminder that you have an open balance on your personal tab that needs to be paid." -msgstr "" +msgstr "Deze e-mail dient als herinnering dat je nog een open saldo hebt op je streeplijst, die nog betaald moet worden." #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:10 msgid "Because you do not have an active, signed mandate for consumptions, we cannot collect this money from your bank automatically, so you will need to pay manually. Please visit the association room or contact the board to pay the balance by cash, bank transfer or PIN." -msgstr "" +msgstr "Omdat je geen actieve, ondertekende machtiging hebt voor consumpties, kunnen we niet automatisch geld van je bankrenening incasseren, en zul je handmatig moeten betalen. Kom een keer langs in de verenigingskamer of neem contact op met het bestuur om je saldo contant, via bankoverschrijving of met PIN te betalen." #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:12 #, python-format msgid "Your open balance is %(costs)s euro. This consists of %(num_unpaid)s transactions." -msgstr "" +msgstr "Je openstaande saldo is %(costs)s euro. Dit bestaat uit %(num_unpaid)s transacties." #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:14 #, python-format msgid "To view more details about the balance and the unpaid transactions, you can view your unpaid transaction overview." -msgstr "" +msgstr "Om meer details te zien over je saldo en de onbetaalde transacties, kan je je eigen onbetaalde transactieoverzicht bekijken." #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:15 #, python-format msgid "To view more details about the balance and the unpaid transactions, you can view your unpaid transaction overview at %(link)s" -msgstr "" +msgstr "Om meer details te zien over je saldo en de onbetaalde transacties, kan je je eigen onbetaalde transactieoverzicht bekijken op %(link)s" #: amelie/personal_tab/templates/personal_tab/balance_reminder.mail:19 -#, fuzzy -#| msgid "" -#| "Kind regards,\n" -#| "\n" -#| "The board of Inter-Actief" msgid "" "Kind regards,\n" "\n" @@ -14560,7 +14186,7 @@ msgid "" msgstr "" "Met vriendelijke groet,\n" "\n" -"Het bestuur van Inter-Actief" +"De penningmeester van Inter-Actief" #: amelie/personal_tab/templates/pos/base.html:6 msgid "Inter-/Actief/ Cookie Corner Point of Sale" @@ -15064,28 +14690,22 @@ msgid "Error, could not scan card." msgstr "Error, kon kaart niet scannen." #: amelie/personal_tab/templates/register/register_card_index.html:41 -#, fuzzy -#| msgid "Scanned card, please wait..." msgid "Received card, please wait..." -msgstr "Kaart gescand, een moment geduld aub..." +msgstr "Kaart ontvangen, een moment geduld aub..." #: amelie/personal_tab/templates/register/register_card_index.html:53 msgid "Connecting to card reader..." msgstr "Verbinden met kaartlezer..." #: amelie/personal_tab/templates/register/register_card_index.html:60 -#, fuzzy -#| msgid "" -#| "\n" -#| " Are you sure you want to remove the alias %(object)s?\n" -#| " " msgid "" "\n" " By registering your card, you can use it to make purchases in the Cookie Corner or at our drinks.\n" " " msgstr "" "\n" -"Weet je zeker dat je alias %(object)s wilt verwijderen?" +" Door je kaart te registreren kan je deze gebruikem om aankopen te doen in de Cookie Corner of op onze borrels.\n" +" " #: amelie/personal_tab/templates/statistics/statistics.html:23 msgid "End date (till)" @@ -15122,8 +14742,6 @@ msgid "Question" msgstr "Vraag" #: amelie/personal_tab/templates/transactions/activity_transaction_detail.html:27 -#, fuzzy -#| msgid "Respons" msgid "Response" msgstr "Antwoord" @@ -15199,10 +14817,8 @@ msgid "Price Total" msgstr "Totaalprijs" #: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:12 -#, fuzzy -#| msgid "You are about to forgive the membership fee of the following persons:" msgid "Use these buttons to send a mailing or forgive the membership fee of the selected members." -msgstr "Je staat op het punt de contributie kwijt te schelden voor de volgende personen:" +msgstr "Gebruik deze knoppen om een mailing te versturen of om de contributie kwijt te schelden voor de geselecteerde personen." #: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:16 msgid "Forgive membership fee" @@ -15213,14 +14829,12 @@ msgid "Debit Refused?" msgstr "Incasso geweigerd?" #: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:94 -#, fuzzy -#| msgid "Unpaid memberships" msgid "No unpaid memberships" -msgstr "Onbetaalde lidmaatschappen" +msgstr "Geen onbetaalde lidmaatschappen" #: amelie/personal_tab/templates/unpaid_memberships/year_overview.html:96 msgid "There are no unpaid memberships in this year! Woah! 🎉🎉🎉" -msgstr "" +msgstr "Er zijn geen onbetaalde lidmaatschappen in dit jaar! Wow! 🎉🎉🎉" #: amelie/personal_tab/templates/wrapped/wrapped.html:4 #: amelie/personal_tab/templates/wrapped/wrapped.html:10 @@ -15404,10 +15018,9 @@ msgid "Look back on a different year?" msgstr "Een ander jaar terugkijken?" #: amelie/personal_tab/transactions.py:74 -#, fuzzy, python-brace-format -#| msgid "Edit enrollment options for %(activity)s" +#, python-brace-format msgid "Settlement of previous costs for {activity}" -msgstr "Inschrijfopties voor %(activity)s wijzigen" +msgstr "Schikking van vorige kosten van {activity}" #: amelie/personal_tab/transactions.py:105 #, python-brace-format @@ -15446,16 +15059,14 @@ msgid "No members were selected." msgstr "Er zijn geen leden geselecteerd." #: amelie/personal_tab/views.py:643 -#, fuzzy, python-brace-format -#| msgid "Contribution {membership_type} ({begin_year}/{end_year})" +#, python-brace-format msgid "Forgiven membership costs for {membership_type} ({begin_year}/{end_year})" -msgstr "Contributie {membership_type} ({begin_year}/{end_year})" +msgstr "Kwijtgescholden lidmaatschapskosten voor {membership_type} ({begin_year}/{end_year})" #: amelie/personal_tab/views.py:660 -#, fuzzy, python-brace-format -#| msgid "The membership fees have been forgiven." +#, python-brace-format msgid "The membership fees for {amount} memberships have been forgiven." -msgstr "De contributie is kwijtgescholden." +msgstr "De contributie voor {amount} lidmaatschappen is kwijtgescholden." #: amelie/personal_tab/views.py:766 amelie/personal_tab/views.py:2323 msgid "The e-mails are now being sent one by one. This happens in a background process, so it may take a while." @@ -15479,64 +15090,54 @@ msgid "Invalid date" msgstr "Ongeldige datum" #: amelie/personal_tab/views.py:1203 -#, fuzzy -#| msgid "Personal tab statistics" msgid "Personal tab transactions in this settlement" -msgstr "Streeplijststatistieken" +msgstr "Streeplijsttransacties in deze schikking" #: amelie/personal_tab/views.py:1211 -#, fuzzy -#| msgid "No other transactions." msgid "Payment for the transactions above" -msgstr "Geen overige transacties." +msgstr "Betaling voor bovenstaande transacties" #: amelie/personal_tab/views.py:1219 -#, fuzzy -#| msgid "Transactions from personal tab:" msgid "Residual balance debited from personal tab" -msgstr "Transacties uit koekenkast:" +msgstr "Overgebleven saldo afgeschreven van streeplijst" #: amelie/personal_tab/views.py:1221 -#, fuzzy -#| msgid "Add transaction to personal tab" msgid "Residual balance credited to personal tab" -msgstr "Streeplijsttransactie toevoegen" +msgstr "Overgebleven saldo bijgeschreven op streeplijst" #: amelie/personal_tab/views.py:1281 #, python-brace-format msgid "Manual payment '{payment}' of '{person}' deleted." -msgstr "" +msgstr "Handmatige betaling '{payment}' van '{person}' is verwijderd." #: amelie/personal_tab/views.py:1363 #, python-brace-format msgid "Personal tab settlement on {date} for {name}" -msgstr "" +msgstr "Streeplijstschikking op {date} voor {name}" #: amelie/personal_tab/views.py:1368 -#, fuzzy, python-brace-format -#| msgid "Mandate form #{id} for {name}" +#, python-brace-format msgid "Refund on {date} for {name}" -msgstr "Machtigingsformulier #{id} voor {name}" +msgstr "Terugbetaling op {date} voor {name}" #: amelie/personal_tab/views.py:1373 -#, fuzzy, python-brace-format -#| msgid "Mandate form #{id} for {name}" +#, python-brace-format msgid "Manual payment on {date} for {name}" -msgstr "Machtigingsformulier #{id} voor {name}" +msgstr "Handmatige betaling op {date} voor {name}" #: amelie/personal_tab/views.py:1379 #, python-brace-format msgid "Debit from personal tab balance on {date} for {name}" -msgstr "" +msgstr "Afschrijving van streeplijstsaldo op {date} voor {name}" #: amelie/personal_tab/views.py:1384 #, python-brace-format msgid "Credit to personal tab balance on {date} for {name}" -msgstr "" +msgstr "Tegoed op streeplijstsaldo op {date} voor {name}" #: amelie/personal_tab/views.py:1404 msgid "No settlement was created because no transactions were selected and no amount was paid." -msgstr "" +msgstr "Er is geen schikking aangemaakt omdat er geen transacties zijn geselecteerd, en er is geen bedrag betaald." #: amelie/personal_tab/views.py:1525 msgid "Invalid statistics list" @@ -18018,32 +17619,25 @@ msgstr "" "Je lidmaatschapstype is %(type)s." #: templates/profile_overview.html:62 -#, fuzzy -#| msgid "" -#| "\n" -#| " Your membership type is %(type)s.\n" -#| " " msgid "" "\n" " Your membership type does not require payment.\n" " " msgstr "" "\n" -"Je lidmaatschapstype is %(type)s." +" Je lidmaatschapstype heeft geen betaling nodig.\n" +" " #: templates/profile_overview.html:68 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Your membership fees have been paid through %(payment_type)s on %(date)s.\n" -#| " " +#, python-format msgid "" "\n" " Your membership fees have been paid through %(method)s on %(date)s.\n" " " msgstr "" "\n" -"Je lidmaatschap is betaald via %(payment_type)s op %(date)s." +" Je lidmaatschap is betaald via %(method)s op %(date)s.\n" +" " #: templates/profile_overview.html:75 msgid "" From 2a2e95de2c54d9d619b7379f54dfe08fb70efd38 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Mon, 17 Aug 2026 18:49:12 +0200 Subject: [PATCH 39/39] Fix migration issue --- amelie/personal_tab/models.py | 4 ++-- .../templates/cookie_corner_unpaid_transactions_list.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amelie/personal_tab/models.py b/amelie/personal_tab/models.py index 6f9e63a2..4e295998 100644 --- a/amelie/personal_tab/models.py +++ b/amelie/personal_tab/models.py @@ -958,8 +958,8 @@ class PaymentMethod(models.Model): description_nl = models.TextField(verbose_name=_l('Description (NL)')) description_en = models.TextField(verbose_name=_l('Description (EN)')) visible = models.BooleanField(default=True, verbose_name=_l('Visible'), help_text=_l("This payment method will be visible when creating a manual payment directly in someone's personal tab.")) - visible_memberships = models.BooleanField(default=True, verbose_name=_l('Visible for memberships'), help_text=_('This payment method will be visible when privileged members enter a manual payment for a membership.')) - visible_activities = models.BooleanField(default=True, verbose_name=_l('Visible for activities'), help_text=_('This payment method will be visible when privileged members enter a manual payment for an activity.')) + visible_memberships = models.BooleanField(default=True, verbose_name=_l('Visible for memberships'), help_text=_l('This payment method will be visible when privileged members enter a manual payment for a membership.')) + visible_activities = models.BooleanField(default=True, verbose_name=_l('Visible for activities'), help_text=_l('This payment method will be visible when privileged members enter a manual payment for an activity.')) frontend_icon_name = models.CharField(max_length=20, blank=True, null=True, verbose_name=_l('Icon name for frontend')) class Meta(object): diff --git a/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html b/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html index 4c6b217c..7490f20c 100644 --- a/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html +++ b/amelie/personal_tab/templates/cookie_corner_unpaid_transactions_list.html @@ -24,7 +24,7 @@

      {% blocktrans with page=page_obj.number total=page_obj.paginator.num_pages % {% if filter_only_mandate %}

      {% if filter_mandate_type %} {% blocktrans with filtered_type=filter_mandate_type %} - Only people with a mandate of type 'filtered_type' are shown. + Only people with a mandate of type '{{ filtered_type }}' are shown. {% endblocktrans %} {% else %} {% trans 'Only people with a mandate are shown.' %}