From 19eb0953d5fa91bd22ae7dd8f7ded01412b68396 Mon Sep 17 00:00:00 2001
From: Christian Merten
Date: Sun, 29 Dec 2024 00:05:00 +0100
Subject: [PATCH 01/21] tests: fix, generate html coverage report
---
docker/test/docker-compose.yaml | 4 +--
docker/test/entrypoint-master.sh | 4 +--
jdav_web/finance/tests.py | 14 ++++-----
jdav_web/members/tests.py | 50 ++++++++++++++++++++----------
jdav_web/startpage/tests.py | 53 ++++++++++++++++++++------------
5 files changed, 78 insertions(+), 47 deletions(-)
diff --git a/docker/test/docker-compose.yaml b/docker/test/docker-compose.yaml
index 9bc9bae..edbb892 100644
--- a/docker/test/docker-compose.yaml
+++ b/docker/test/docker-compose.yaml
@@ -14,8 +14,8 @@ services:
entrypoint: /app/docker/test/entrypoint-master.sh
volumes:
- type: bind
- source: ./coverage.xml
- target: /app/jdav_web/coverage.xml
+ source: ./htmlcov/
+ target: /app/jdav_web/htmlcov/
cache:
restart: always
diff --git a/docker/test/entrypoint-master.sh b/docker/test/entrypoint-master.sh
index 8c70043..2c80809 100755
--- a/docker/test/entrypoint-master.sh
+++ b/docker/test/entrypoint-master.sh
@@ -38,5 +38,5 @@ fi
cd jdav_web
-coverage run manage.py test startpage finance members -v 2
-coverage xml
+coverage run manage.py test startpage finance members contrib logindata mailer material -v 2 --noinput
+coverage html
diff --git a/jdav_web/finance/tests.py b/jdav_web/finance/tests.py
index 0014d8d..08b531a 100644
--- a/jdav_web/finance/tests.py
+++ b/jdav_web/finance/tests.py
@@ -3,7 +3,7 @@ from django.utils import timezone
from django.conf import settings
from .models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction
from members.models import Member, Group, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, NewMemberOnList,\
- FAHRGEMEINSCHAFT_ANREISE
+ FAHRGEMEINSCHAFT_ANREISE, MALE, FEMALE, DIVERSE
# Create your tests here.
class StatementTestCase(TestCase):
@@ -11,11 +11,11 @@ class StatementTestCase(TestCase):
kilometers_traveled = 512
participant_count = 10
staff_count = 5
-
+
def setUp(self):
self.jl = Group.objects.create(name="Jugendleiter")
self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
self.fritz.group.add(self.jl)
self.fritz.save()
@@ -39,12 +39,12 @@ class StatementTestCase(TestCase):
self.st3 = Statement.objects.create(night_cost=self.night_cost, excursion=ex)
for i in range(self.participant_count):
m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
mol = NewMemberOnList.objects.create(member=m, memberlist=ex)
ex.membersonlist.add(mol)
for i in range(self.staff_count):
m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
Bill.objects.create(statement=self.st3, short_description='food', explanation='i was hungry',
amount=42.69, costs_covered=True, paid_by=m)
m.group.add(self.jl)
@@ -57,7 +57,7 @@ class StatementTestCase(TestCase):
self.st4 = Statement.objects.create(night_cost=self.night_cost, excursion=ex)
for i in range(2):
m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=DIVERSE)
mol = NewMemberOnList.objects.create(member=m, memberlist=ex)
ex.membersonlist.add(mol)
@@ -66,7 +66,7 @@ class StatementTestCase(TestCase):
'Admissible staff count is not 0, although not enough participants.')
for i in range(2):
m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=DIVERSE)
mol = NewMemberOnList.objects.create(member=m, memberlist=self.st4.excursion)
self.st4.excursion.membersonlist.add(mol)
self.assertEqual(self.st4.admissible_staff_count, 2,
diff --git a/jdav_web/members/tests.py b/jdav_web/members/tests.py
index 673d658..440446f 100644
--- a/jdav_web/members/tests.py
+++ b/jdav_web/members/tests.py
@@ -6,8 +6,10 @@ from django.test import TestCase, Client, RequestFactory
from django.utils import timezone, translation
from django.conf import settings
from django.urls import reverse
+from unittest import skip
from .models import Member, Group, PermissionMember, PermissionGroup, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE,\
- MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact
+ MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact,\
+ DIVERSE, MALE, FEMALE
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
@@ -18,7 +20,7 @@ def create_custom_user(username, groups, prename, lastname):
user = User.objects.create_user(
username=username, password='secret'
)
- member = Member.objects.create(prename=prename, lastname=lastname, birth_date=timezone.localdate(), email=settings.TEST_MAIL)
+ member = Member.objects.create(prename=prename, lastname=lastname, birth_date=timezone.localdate(), email=settings.TEST_MAIL, gender=DIVERSE)
member.user = user
member.save()
user.is_staff = True
@@ -37,22 +39,22 @@ class BasicMemberTestCase(TestCase):
self.spiel = Group.objects.create(name="Spielkinder")
self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=DIVERSE)
self.fritz.group.add(self.jl)
self.fritz.group.add(self.alp)
self.fritz.save()
self.lara = Member.objects.create(prename="Lara", lastname="Wallis", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=DIVERSE)
self.lara.group.add(self.alp)
self.lara.save()
self.fridolin = Member.objects.create(prename="Fridolin", lastname="Spargel", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
self.fridolin.group.add(self.alp)
self.fridolin.group.add(self.spiel)
self.fridolin.save()
self.lise = Member.objects.create(prename="Lise", lastname="Lotte", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=FEMALE)
class MemberTestCase(BasicMemberTestCase):
@@ -66,11 +68,11 @@ class MemberTestCase(BasicMemberTestCase):
self.ja = Group.objects.create(name="Jugendausschuss")
self.peter = Member.objects.create(prename="Peter", lastname="Keks", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
self.anna = Member.objects.create(prename="Anna", lastname="Keks", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=FEMALE)
self.lisa = Member.objects.create(prename="Lisa", lastname="Keks", birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=DIVERSE)
self.peter.group.add(self.ja)
self.anna.group.add(self.ja)
self.lisa.group.add(self.ja)
@@ -128,7 +130,7 @@ class PDFTestCase(TestCase):
for i in range(7):
m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=FEMALE)
NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.ex)
NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.note)
@@ -158,6 +160,13 @@ class PDFTestCase(TestCase):
self._test_pdf('notes_list')
self._test_pdf('notes_list', username='standard', invalid=True)
+ def test_sjr_application(self):
+ self._test_pdf('sjr_application')
+ self._test_pdf('sjr_application', username='standard', invalid=True)
+
+ # TODO: Since generating a seminar report requires more input now, this test rightly
+ # fails. Replace this test with one that fills the POST form and generates a pdf.
+ @skip("Currently rightly fails, because expected behaviour changed.")
def test_seminar_report(self):
self._test_pdf('seminar_report')
self._test_pdf('seminar_report', username='standard', invalid=True)
@@ -200,21 +209,21 @@ class AdminTestCase(TestCase):
for i in range(3):
m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
m.group.add(cool_kids)
m.save()
for i in range(7):
m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=FEMALE)
m.group.add(super_kids)
m.save()
for i in range(5):
m = Member.objects.create(prename='Lulla {}'.format(i), lastname='Hulla', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=DIVERSE)
m.group.add(staff)
m.save()
m = Member.objects.create(prename='Peter', lastname='Hulla', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
m.group.add(staff)
p1.list_members.add(m)
@@ -256,7 +265,7 @@ class MemberAdminTestCase(AdminTestCase):
for i in range(1):
m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=MALE)
m.group.add(mega_kids)
m.save()
@@ -383,7 +392,7 @@ class FreizeitAdminTestCase(AdminTestCase):
for i in range(7):
m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(),
- email=settings.TEST_MAIL)
+ email=settings.TEST_MAIL, gender=FEMALE)
NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=ex)
def test_changelist(self):
@@ -413,11 +422,17 @@ class FreizeitAdminTestCase(AdminTestCase):
response = c.get(url)
self.assertEqual(response.status_code, 200, 'Response code is not 200.')
+ @skip("The filtering is currently (intentionally) disabled.")
+ def test_add_queryset_filter(self):
+ """Test if queryset on `jugendleiter` field is properly filtered by permissions."""
u = User.objects.get(username='standard')
+ c = self._login('standard')
+
+ url = reverse('admin:members_freizeit_add')
request = self.factory.get(url)
request.user = u
- #staff = Group.objects.get(name='Jugendleiter')
+
field = Freizeit._meta.get_field('jugendleiter')
queryset = self.admin.formfield_for_manytomany(field, request).queryset
self.assertQuerysetEqual(queryset, u.member.filter_queryset_by_permissions(model=Member),
@@ -472,6 +487,7 @@ class MailConfirmationTestCase(BasicMemberTestCase):
# father's mail should now be confirmed
self.assertTrue(self.father.confirmed_mail, msg='After confirming by key, the mail should be confirmed.')
+ @skip("Currently, emergency contact email addresses are not required to be confirmed.")
def test_emergency_contact_confirmation(self):
# request mail confirmation of fritz, should also ask for confirmation of father
requested_confirmation = self.fritz.request_mail_confirmation()
diff --git a/jdav_web/startpage/tests.py b/jdav_web/startpage/tests.py
index c012738..8a8ceef 100644
--- a/jdav_web/startpage/tests.py
+++ b/jdav_web/startpage/tests.py
@@ -1,46 +1,55 @@
from django.test import TestCase, Client
from django.urls import reverse
+from django.conf import settings
from members.models import Group
from .models import Post, Section
-class ModelsTestCase(TestCase):
+class BasicTestCase(TestCase):
def setUp(self):
orga = Section.objects.create(title='Organisation', urlname='orga', website_text='Section is a about everything.')
- Post.objects.create(title='Climbing is fun', urlname='climbing-is-fun', website_text='Climbing is fun!')
+ recent = Section.objects.create(title='Recent', urlname=settings.RECENT_SECTION, website_text='Recently recent.')
+ reports = Section.objects.create(title='Reports', urlname=settings.REPORTS_SECTION, website_text='Reporty reports.')
+ Post.objects.create(title='Climbing is fun', urlname='climbing-is-fun', website_text='Climbing is fun!',
+ section=recent)
+ Post.objects.create(title='Last trip', urlname='last-trip', website_text='A fun trip.',
+ section=reports)
Post.objects.create(title='Staff', urlname='staff', website_text='This is our staff: Peter.',
section=orga)
+ Group.objects.create(name='CrazyClimbers', show_website=True)
+ Group.objects.create(name='SuperClimbers', show_website=False)
+
+class ModelsTestCase(BasicTestCase):
def test_str(self):
orga = Section.objects.get(urlname='orga')
self.assertEqual(str(orga), orga.title, 'String representation does not match title.')
post = Post.objects.get(urlname='staff', section=orga)
self.assertEqual(post.absolute_section(), orga.title, 'Displayed section of post does not match section title.')
self.assertEqual(str(post), post.title, 'String representation does not match title.')
- for post in Post.objects.filter(section=None):
- self.assertEqual(post.absolute_section(), 'Aktuelles', 'Displayed section of post does not "Aktuelles".')
def test_absolute_urlnames(self):
orga = Section.objects.get(urlname='orga')
+ recent = Section.objects.get(urlname=settings.RECENT_SECTION)
+ reports = Section.objects.get(urlname=settings.REPORTS_SECTION)
self.assertEqual(orga.absolute_urlname(), '/de/orga')
post1 = Post.objects.get(urlname='staff', section=orga)
self.assertEqual(post1.absolute_urlname(), '/de/orga/staff')
- post2 = Post.objects.get(urlname='climbing-is-fun', section=None)
- self.assertEqual(post2.absolute_urlname(), '/de/aktuelles/climbing-is-fun')
-
-
-class ViewTestCase(TestCase):
- def setUp(self):
- orga = Section.objects.create(title='Organisation', urlname='orga', website_text='Section is a about everything.')
- Post.objects.create(title='Climbing is fun', urlname='climbing-is-fun', website_text='Climbing is fun!')
- Post.objects.create(title='Staff', urlname='staff', website_text='This is our staff: Peter.',
- section=orga)
- Group.objects.create(name='CrazyClimbers', show_website=True)
- Group.objects.create(name='SuperClimbers', show_website=False)
-
+ self.assertEqual(post1.absolute_urlname(), reverse('startpage:post', args=(orga.urlname, 'staff')))
+ post2 = Post.objects.get(urlname='climbing-is-fun', section=recent)
+ self.assertEqual(post2.absolute_urlname(),
+ '/de/{name}/climbing-is-fun'.format(name=settings.RECENT_SECTION))
+ self.assertEqual(post2.absolute_urlname(), reverse('startpage:post', args=(recent.urlname, 'climbing-is-fun')))
+ post3 = Post.objects.get(urlname='last-trip', section=reports)
+ self.assertEqual(post3.absolute_urlname(),
+ '/de/{name}/last-trip'.format(name=settings.REPORTS_SECTION))
+ self.assertEqual(post3.absolute_urlname(), reverse('startpage:post', args=(reports.urlname, 'last-trip')))
+
+
+class ViewTestCase(BasicTestCase):
def test_index(self):
c = Client()
url = reverse('startpage:index')
@@ -49,7 +58,7 @@ class ViewTestCase(TestCase):
def test_posts_no_category(self):
c = Client()
- url = reverse('startpage:post', args=('aktuelles', 'climbing-is-fun'))
+ url = reverse('startpage:post', args=(settings.RECENT_SECTION, 'climbing-is-fun'))
response = c.get(url)
self.assertEqual(response.status_code, 200, 'Response code is not 200 for climbing post.')
@@ -67,7 +76,13 @@ class ViewTestCase(TestCase):
def test_section_recent(self):
c = Client()
- url = reverse('startpage:aktuelles')
+ url = reverse('startpage:' + settings.RECENT_SECTION)
+ response = c.get(url)
+ self.assertEqual(response.status_code, 200, 'Response code is not 200 for section page.')
+
+ def test_section_reports(self):
+ c = Client()
+ url = reverse('startpage:' + settings.REPORTS_SECTION)
response = c.get(url)
self.assertEqual(response.status_code, 200, 'Response code is not 200 for section page.')
From 3b46695b49ca8b3a5cd29ceb6248f259d59834a9 Mon Sep 17 00:00:00 2001
From: "marius.klein"
Date: Sun, 29 Dec 2024 22:48:31 +0100
Subject: [PATCH 02/21] finance/admin: validate IBAN and show EPC-QR code for
transactions (#94)
1. IBAN validation in member admin.
2. In the transaction overview, for every transaction an EPC-QR code for banking apps is generated and displayed. The (necessary) BIC field is automatically derived from the IBAN. This closes #63.
Both steps use the python library schwifty.
Reviewed-on: https://git.jdav-hd.merten.dev/digitales/kompass/pulls/94
Reviewed-by: Christian Merten
Co-authored-by: marius.klein
Co-committed-by: marius.klein
---
.../finance/locale/de/LC_MESSAGES/django.po | 149 +++--
jdav_web/finance/models.py | 42 ++
.../templates/admin/confirmed_statement.html | 45 ++
jdav_web/members/admin.py | 20 +-
.../members/locale/de/LC_MESSAGES/django.po | 156 ++---
jdav_web/static/js/qrcode.js | 614 ++++++++++++++++++
requirements.txt | 1 +
7 files changed, 884 insertions(+), 143 deletions(-)
create mode 100644 jdav_web/static/js/qrcode.js
diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
index cc5404f..5da218d 100644
--- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-01 16:23+0100\n"
+"POT-Creation-Date: 2024-12-28 01:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -18,12 +18,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: finance/admin.py:76
+#: finance/admin.py:84
#, python-format
msgid "%(name)s is already submitted."
msgstr "%(name)s ist bereits eingereicht."
-#: finance/admin.py:82
+#: finance/admin.py:90
#, python-format
msgid ""
"Successfully submited %(name)s. The finance department will notify the "
@@ -32,23 +32,23 @@ msgstr ""
"Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich "
"sobald wie möglich zukommen."
-#: finance/admin.py:85
+#: finance/admin.py:93
msgid "Submit statement"
msgstr "Rechnung einreichen"
-#: finance/admin.py:162
+#: finance/admin.py:177
#, python-format
msgid "%(name)s is not yet submitted."
msgstr "%(name)s ist noch nicht eingereicht."
-#: finance/admin.py:169
+#: finance/admin.py:184
#, python-format
msgid "An error occured while trying to confirm %(name)s. Please try again."
msgstr ""
"Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es "
"erneut."
-#: finance/admin.py:173
+#: finance/admin.py:188
#, python-format
msgid ""
"Successfully confirmed %(name)s. I hope you executed the associated "
@@ -57,11 +57,11 @@ msgstr ""
"Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen "
"Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern."
-#: finance/admin.py:180
+#: finance/admin.py:195
msgid "Statement confirmed"
msgstr "Abrechnung abgewickelt"
-#: finance/admin.py:186
+#: finance/admin.py:201
msgid ""
"Transactions do not match the covered expenses. Please correct the mistakes "
"listed below."
@@ -69,19 +69,19 @@ msgstr ""
"Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte "
"korrigiere die unten aufgeführten Fehler."
-#: finance/admin.py:191
+#: finance/admin.py:206
msgid "Some transactions have no ledger configured. Please fill in the gaps."
msgstr ""
"Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach."
-#: finance/admin.py:200
+#: finance/admin.py:215
#, python-format
msgid "Successfully rejected %(name)s. The requestor can reapply, when needed."
msgstr ""
"Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut "
"einstellen, wenn es benötigt wird."
-#: finance/admin.py:207
+#: finance/admin.py:222
#, python-format
msgid ""
"%(name)s already has transactions. Please delete them first, if you want to "
@@ -90,12 +90,12 @@ msgstr ""
"%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du "
"neue generierst."
-#: finance/admin.py:212
+#: finance/admin.py:227
#, python-format
msgid "Successfully generated transactions for %(name)s"
msgstr "Automatisch Überweisungsträger für %(name)s generiert."
-#: finance/admin.py:215
+#: finance/admin.py:230
#, python-format
msgid ""
"Error while generating transactions for %(name)s. Do all bills have a payer?"
@@ -103,28 +103,28 @@ msgstr ""
"Fehler beim Erzeugen der Überweisungsträger für %(name)s. Sind für alle "
"Quittungen eine bezahlende Person eingestellt? "
-#: finance/admin.py:218
+#: finance/admin.py:233
msgid "View submitted statement"
msgstr "Eingereichte Abrechnung einsehen"
-#: finance/admin.py:230
+#: finance/admin.py:245
#, python-format
msgid "Successfully reduced transactions for %(name)s."
msgstr "Überweisungsträger für %(name)s minimiert."
-#: finance/admin.py:274
+#: finance/admin.py:293
#, python-format
msgid "%(name)s is not yet confirmed."
msgstr "%(name)s ist noch nicht bestätigt."
-#: finance/admin.py:283
+#: finance/admin.py:302
#, python-format
msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing."
msgstr ""
"Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt "
"was du machst."
-#: finance/admin.py:288 finance/templates/admin/unconfirm_statement.html:26
+#: finance/admin.py:307 finance/templates/admin/unconfirm_statement.html:26
msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen"
@@ -132,185 +132,185 @@ msgstr "Bestätigung zurücknehmen"
msgid "Finance"
msgstr "Finanzen"
-#: finance/models.py:21
+#: finance/models.py:24
msgid "Name"
msgstr "Name"
-#: finance/models.py:27 finance/models.py:472 finance/models.py:496
-#: finance/templates/admin/confirmed_statement.html:38
+#: finance/models.py:30 finance/models.py:484 finance/models.py:547
+#: finance/templates/admin/confirmed_statement.html:40
#: finance/templates/admin/overview_submitted_statement.html:100
msgid "Ledger"
msgstr "Geldtopf"
-#: finance/models.py:28
+#: finance/models.py:31
msgid "Ledgers"
msgstr "Geldtöpfe"
-#: finance/models.py:48 finance/models.py:415 finance/models.py:495
+#: finance/models.py:51 finance/models.py:420 finance/models.py:546
msgid "Short description"
msgstr "Kurzbeschreibung"
-#: finance/models.py:51 finance/models.py:416
+#: finance/models.py:54 finance/models.py:421
msgid "Explanation"
msgstr "Erklärung"
-#: finance/models.py:53
+#: finance/models.py:56
msgid "Associated excursion"
msgstr "Zugehörige Ausfahrt"
-#: finance/models.py:58
+#: finance/models.py:61
msgid "Price per night"
msgstr "Preis pro Nacht"
-#: finance/models.py:60
+#: finance/models.py:63
msgid "Submitted"
msgstr "Eingericht"
-#: finance/models.py:61
+#: finance/models.py:64
msgid "Submitted on"
msgstr "Eingereicht am"
-#: finance/models.py:62
+#: finance/models.py:65
msgid "Confirmed"
msgstr "Abgewickelt"
-#: finance/models.py:63 finance/models.py:479
+#: finance/models.py:66 finance/models.py:491
msgid "Paid on"
msgstr "Bezahlt am"
-#: finance/models.py:65
+#: finance/models.py:68
msgid "Created by"
msgstr "Erstellt von"
-#: finance/models.py:70
+#: finance/models.py:73
msgid "Submitted by"
msgstr "Eingereicht von"
-#: finance/models.py:75 finance/models.py:480
+#: finance/models.py:78 finance/models.py:492
msgid "Authorized by"
msgstr "Autorisiert von"
-#: finance/models.py:82 finance/models.py:414 finance/models.py:475
+#: finance/models.py:85 finance/models.py:419 finance/models.py:487
msgid "Statement"
msgstr "Abrechnung"
-#: finance/models.py:83
+#: finance/models.py:86
msgid "Statements"
msgstr "Abrechnungen"
-#: finance/models.py:98
+#: finance/models.py:101
#, python-format
msgid "Statement: %(excursion)s"
msgstr "Abrechnung: %(excursion)s"
-#: finance/models.py:150
+#: finance/models.py:153
msgid "Ready to confirm"
msgstr "Bereit zur Abwicklung"
-#: finance/models.py:194
+#: finance/models.py:197
#, python-format
msgid "Compensation for %(excu)s"
msgstr "Entschädigung für %(excu)s"
-#: finance/models.py:327
+#: finance/models.py:330
#: finance/templates/admin/overview_submitted_statement.html:78
msgid "Total"
msgstr "Gesamtbetrag"
-#: finance/models.py:369
+#: finance/models.py:374
msgid "Statement in preparation"
msgstr "Abrechnung in Vorbereitung"
-#: finance/models.py:370
+#: finance/models.py:375
msgid "Statements in preparation"
msgstr "Abrechnungen in Vorbereitung"
-#: finance/models.py:389
+#: finance/models.py:394
msgid "Submitted statement"
msgstr "Eingereichte Abrechnung"
-#: finance/models.py:390
+#: finance/models.py:395
msgid "Submitted statements"
msgstr "Eingereichte Abrechnungen"
-#: finance/models.py:406
+#: finance/models.py:411
msgid "Paid statement"
msgstr "Bezahlte Abrechnung"
-#: finance/models.py:407
+#: finance/models.py:412
msgid "Paid statements"
msgstr "Bezahlte Abrechnungen"
-#: finance/models.py:418 finance/models.py:432 finance/models.py:469
-#: finance/templates/admin/confirmed_statement.html:36
+#: finance/models.py:423 finance/models.py:444 finance/models.py:481
+#: finance/templates/admin/confirmed_statement.html:38
#: finance/templates/admin/overview_submitted_statement.html:31
#: finance/templates/admin/overview_submitted_statement.html:98
msgid "Amount"
msgstr "Betrag"
-#: finance/models.py:419
+#: finance/models.py:424
msgid "Paid by"
msgstr "Bezahlt von"
-#: finance/models.py:421
+#: finance/models.py:426
msgid "Covered"
msgstr "Übernommen"
-#: finance/models.py:422
+#: finance/models.py:427
msgid "Refunded"
msgstr "Ausgezahlt"
-#: finance/models.py:424
+#: finance/models.py:429
msgid "Proof"
msgstr "Beleg"
-#: finance/models.py:435 finance/models.py:442 finance/models.py:455
+#: finance/models.py:447 finance/models.py:454 finance/models.py:467
msgid "Bill"
msgstr "Ausgabe"
-#: finance/models.py:436 finance/models.py:443 finance/models.py:456
+#: finance/models.py:448 finance/models.py:455 finance/models.py:468
#: finance/templates/admin/overview_submitted_statement.html:26
msgid "Bills"
msgstr "Ausgaben"
-#: finance/models.py:468 finance/templates/admin/confirmed_statement.html:37
+#: finance/models.py:480 finance/templates/admin/confirmed_statement.html:39
#: finance/templates/admin/overview_submitted_statement.html:99
msgid "Reference"
msgstr "Verwendungszweck"
-#: finance/models.py:470
+#: finance/models.py:482
msgid "Recipient"
msgstr "Empfänger"
-#: finance/models.py:478
+#: finance/models.py:490
msgid "Paid"
msgstr "Bezahlt"
-#: finance/models.py:490
+#: finance/models.py:541
msgid "Transaction"
msgstr "Überweisung"
-#: finance/models.py:491
+#: finance/models.py:542
#: finance/templates/admin/overview_submitted_statement.html:84
msgid "Transactions"
msgstr "Überweisungen"
-#: finance/templates/admin/confirmed_statement.html:17
+#: finance/templates/admin/confirmed_statement.html:19
#: finance/templates/admin/overview_submitted_statement.html:17
#: finance/templates/admin/submit_statement.html:17
#: finance/templates/admin/unconfirm_statement.html:17
msgid "Home"
msgstr "Start"
-#: finance/templates/admin/confirmed_statement.html:21
+#: finance/templates/admin/confirmed_statement.html:23
msgid "Paiment"
msgstr "Bezahlung"
-#: finance/templates/admin/confirmed_statement.html:26
+#: finance/templates/admin/confirmed_statement.html:28
msgid "Paying statement"
msgstr "Rechnung bezahlen"
-#: finance/templates/admin/confirmed_statement.html:29
+#: finance/templates/admin/confirmed_statement.html:31
msgid ""
"The statement is valid. Please execute the following transactions and then "
"proceed by finalizing the confirmation."
@@ -318,15 +318,32 @@ msgstr ""
"Die Abrechnung ist gültig. Bitte führe die folgenden Überweisungen aus und "
"fahre dann fort, indem du die Abwicklung bestätigst."
-#: finance/templates/admin/confirmed_statement.html:35
+#: finance/templates/admin/confirmed_statement.html:37
msgid "IBAN"
msgstr "IBAN"
-#: finance/templates/admin/confirmed_statement.html:66
+#: finance/templates/admin/confirmed_statement.html:41
+msgid "QR Code"
+msgstr "QR Code"
+
+#: finance/templates/admin/confirmed_statement.html:61
+#: finance/templates/admin/confirmed_statement.html:98
+msgid "Show"
+msgstr "Anzeigen"
+
+#: finance/templates/admin/confirmed_statement.html:86
+msgid "No QR code can be displayed."
+msgstr "Es kann kein QR-Code angezeigt werden."
+
+#: finance/templates/admin/confirmed_statement.html:99
+msgid "Showing"
+msgstr "Sichtbar"
+
+#: finance/templates/admin/confirmed_statement.html:111
msgid "I did execute the listed transactions."
msgstr "Ich habe die aufgeführten Überweisungen ausgeführt."
-#: finance/templates/admin/confirmed_statement.html:68
+#: finance/templates/admin/confirmed_statement.html:113
msgid "Confirm"
msgstr "Bestätigen"
diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py
index 04286e1..d27b74f 100644
--- a/jdav_web/finance/models.py
+++ b/jdav_web/finance/models.py
@@ -15,6 +15,9 @@ from contrib.models import CommonModel
from contrib.rules import has_global_perm
from utils import cvt_to_decimal, RestrictedFileField
+from schwifty import IBAN
+import re
+
# Create your models here.
class Ledger(models.Model):
@@ -495,6 +498,45 @@ class Transaction(models.Model):
def __str__(self):
return "T#{}".format(self.pk)
+ @staticmethod
+ def escape_reference(reference):
+ umlaut_map = {
+ 'ä': 'ae', 'ö': 'oe', 'ü': 'ue',
+ 'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue',
+ 'ß': 'ss'
+ }
+ pattern = re.compile('|'.join(umlaut_map.keys()))
+ int_reference = pattern.sub(lambda x: umlaut_map[x.group()], reference)
+ allowed_chars = r"[^a-z0-9 /?: .,'+-]"
+ clean_reference = re.sub(allowed_chars, '', int_reference, flags=re.IGNORECASE)
+ return clean_reference
+
+ def code(self):
+
+ if self.amount == 0:
+ return ""
+
+ iban = IBAN(self.member.iban, allow_invalid=True)
+ if not iban.is_valid:
+ return ""
+ bic = iban.bic
+
+ reference = self.escape_reference(self.reference)
+
+ # also escaping receiver as umlaute are also not allowed here
+ receiver = self.escape_reference(f"{self.member.prename} {self.member.lastname}")
+ return f"""BCD
+001
+1
+SCT
+{bic}
+{receiver}
+{iban}
+EUR{self.amount}
+
+
+{reference}"""
+
class Meta:
verbose_name = _('Transaction')
verbose_name_plural = _('Transactions')
diff --git a/jdav_web/finance/templates/admin/confirmed_statement.html b/jdav_web/finance/templates/admin/confirmed_statement.html
index aa2c079..3d35418 100644
--- a/jdav_web/finance/templates/admin/confirmed_statement.html
+++ b/jdav_web/finance/templates/admin/confirmed_statement.html
@@ -7,6 +7,8 @@
+
+
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} admin-view
@@ -36,6 +38,7 @@
| {% trans "Amount" %} |
{% trans "Reference" %} |
{% trans "Ledger" %} |
+ {% trans "QR Code" %} |
{% for transaction in statement.transaction_set.all %}
@@ -54,11 +57,53 @@
|
{{ transaction.ledger }}
|
+
+ {% trans "Show" %}
+ |
{% endfor %}
+
+