feat(finance/admin): unified statement view (#6)

This PR replaces the separate admin views for unsubmitted, submitted and
confirmed statements by one common view. To distinguish the state, we
now display a colored badge in the changelist.

The default permissions for the `Standard` group are changed so that
normal users can continue to view statements they are related to when
these are submitted or confirmed.
main
Christian Merten 1 month ago committed by GitHub
parent 3b8964fbb0
commit 5d4f29be89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -62,17 +62,44 @@ def decorate_statement_view(model, perm=None):
return decorator return decorator
@admin.register(StatementUnSubmitted) @admin.register(Statement)
class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin): class StatementAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'status'] fields = ['short_description', 'explanation', 'excursion', 'status']
list_display = ['__str__', 'excursion', 'created_by'] list_display = ['__str__', 'total_pretty', 'created_by', 'submitted_date', 'is_valid', 'status_badge']
list_filter = ['status']
search_fields = ('excursion__name', 'short_description')
ordering = ['-submitted_date']
inlines = [BillOnStatementInline] inlines = [BillOnStatementInline]
def has_change_permission(self, request, obj=None):
if obj is None:
return super().has_change_permission(request)
if obj.confirmed:
# Confirmed statements may not be changed (they should be unconfirmed first)
return False
return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None):
if obj is None or obj.submitted:
# Submitted statements may not be deleted (they should be rejected first)
return False
return super().has_delete_permission(request, obj)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if not change and hasattr(request.user, 'member'): if not change and hasattr(request.user, 'member'):
obj.created_by = request.user.member obj.created_by = request.user.member
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
def get_fields(self, request, obj=None):
if obj is not None and obj.excursion:
# if the object exists and an excursion is set, show the excursion (read only)
# instead of the short description
return ['excursion', 'explanation', 'status']
else:
# if the object is newly created or no excursion is set, require
# a short description
return ['short_description', 'explanation', 'status']
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
readonly_fields = ['status', 'excursion'] readonly_fields = ['status', 'excursion']
if obj is not None and obj.submitted: if obj is not None and obj.submitted:
@ -80,6 +107,12 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
else: else:
return readonly_fields return readonly_fields
def get_inlines(self, request, obj=None):
if obj is None or not obj.submitted:
return [BillOnStatementInline]
else:
return [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
@ -96,10 +129,30 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
wrap(self.submit_view), wrap(self.submit_view),
name="%s_%s_submit" % (self.opts.app_label, self.opts.model_name), name="%s_%s_submit" % (self.opts.app_label, self.opts.model_name),
), ),
path(
"<path:object_id>/overview/",
wrap(self.overview_view),
name="%s_%s_overview" % (self.opts.app_label, self.opts.model_name),
),
path(
"<path:object_id>/reduce_transactions/",
wrap(self.reduce_transactions_view),
name="%s_%s_reduce_transactions" % (self.opts.app_label, self.opts.model_name),
),
path(
"<path:object_id>/unconfirm/",
wrap(self.unconfirm_view),
name="%s_%s_unconfirm" % (self.opts.app_label, self.opts.model_name),
),
path(
"<path:object_id>/summary/",
wrap(self.statement_summary_view),
name="%s_%s_summary" % (self.opts.app_label, self.opts.model_name),
),
] ]
return custom_urls + urls return custom_urls + urls
@decorate_statement_view(Statement) @decorate_statement_view(StatementUnSubmitted)
def submit_view(self, request, statement): def submit_view(self, request, statement):
if statement.submitted: # pragma: no cover if statement.submitted: # pragma: no cover
logger.error(f"submit_view reached with submitted statement {statement}. This should not happen.") logger.error(f"submit_view reached with submitted statement {statement}. This should not happen.")
@ -131,91 +184,6 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
statement=statement) statement=statement)
return render(request, 'admin/submit_statement.html', context=context) return render(request, 'admin/submit_statement.html', context=context)
class TransactionOnSubmittedStatementInline(admin.TabularInline):
model = Transaction
fields = ['amount', 'member', 'reference', 'text_length_warning', 'ledger']
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
readonly_fields = ['text_length_warning']
extra = 0
def text_length_warning(self, obj):
"""Display reference length, warn if exceeds 140 characters."""
len_reference = len(obj.reference)
len_string = f"{len_reference}/140"
if len_reference > 140:
return mark_safe(f'<span style="color: red;">{len_string}</span>')
return len_string
text_length_warning.short_description = _("Length")
class BillOnSubmittedStatementInline(BillOnStatementInline):
model = BillOnStatementProxy
extra = 0
sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof', 'costs_covered']
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
def get_readonly_fields(self, request, obj=None):
return ['short_description', 'explanation', 'amount', 'paid_by', 'proof']
@admin.register(StatementSubmitted)
class StatementSubmittedAdmin(admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'status']
list_display = ['__str__', 'is_valid', 'submitted_date', 'submitted_by']
ordering = ('-submitted_date',)
inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def has_add_permission(self, request, obj=None):
# Submitted statements should not be added directly, but instead be created
# as unsubmitted statements and then submitted.
return False
def has_change_permission(self, request, obj=None):
return request.user.has_perm('finance.process_statementsubmitted')
def has_delete_permission(self, request, obj=None):
# Submitted statements should not be deleted. Instead they can be rejected
# and then deleted as unsubmitted statements.
return False
def get_readonly_fields(self, request, obj=None):
readonly_fields = ['status']
if obj is not None and obj.submitted:
return readonly_fields + self.fields
else:
return readonly_fields
def get_urls(self):
urls = super().get_urls()
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
custom_urls = [
path(
"<path:object_id>/overview/",
wrap(self.overview_view),
name="%s_%s_overview" % (self.opts.app_label, self.opts.model_name),
),
path(
"<path:object_id>/reduce_transactions/",
wrap(self.reduce_transactions_view),
name="%s_%s_reduce_transactions" % (self.opts.app_label, self.opts.model_name),
),
]
return custom_urls + urls
@decorate_statement_view(StatementSubmitted) @decorate_statement_view(StatementSubmitted)
def overview_view(self, request, statement): def overview_view(self, request, statement):
if not statement.submitted: # pragma: no cover if not statement.submitted: # pragma: no cover
@ -238,7 +206,8 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
messages.success(request, messages.success(request,
_("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.") _("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.")
% {'name': str(statement)}) % {'name': str(statement)})
download_link = reverse('admin:finance_statementconfirmed_summary', args=(statement.pk,)) download_link = reverse('admin:%s_%s_summary' % (self.opts.app_label, self.opts.model_name),
args=(statement.pk,))
messages.success(request, messages.success(request,
mark_safe(_("You can download a <a href='%(link)s', target='_blank'>receipt</a>.") % {'link': download_link})) mark_safe(_("You can download a <a href='%(link)s', target='_blank'>receipt</a>.") % {'link': download_link}))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
@ -310,52 +279,6 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
messages.success(request, messages.success(request,
_("Successfully reduced transactions for %(name)s.") % {'name': str(statement)}) _("Successfully reduced transactions for %(name)s.") % {'name': str(statement)})
return HttpResponseRedirect(request.GET['redirectTo']) return HttpResponseRedirect(request.GET['redirectTo'])
#return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
@admin.register(StatementConfirmed)
class StatementConfirmedAdmin(admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'status']
#readonly_fields = fields
list_display = ['__str__', 'total_pretty', 'confirmed_date', 'confirmed_by']
ordering = ('-confirmed_date',)
inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def has_add_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to add confirmed statements
return False
def has_change_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to change confirmed statements
return False
def has_delete_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to delete confirmed statements
return False
def get_urls(self):
urls = super().get_urls()
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
custom_urls = [
path(
"<path:object_id>/unconfirm/",
wrap(self.unconfirm_view),
name="%s_%s_unconfirm" % (self.opts.app_label, self.opts.model_name),
),
path(
"<path:object_id>/summary/",
wrap(self.statement_summary_view),
name="%s_%s_summary" % (self.opts.app_label, self.opts.model_name),
),
]
return custom_urls + urls
@decorate_statement_view(StatementConfirmed, perm='finance.may_manage_confirmed_statements') @decorate_statement_view(StatementConfirmed, perm='finance.may_manage_confirmed_statements')
def unconfirm_view(self, request, statement): def unconfirm_view(self, request, statement):
@ -399,6 +322,39 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
statement_summary_view.short_description = _('Download summary') statement_summary_view.short_description = _('Download summary')
class TransactionOnSubmittedStatementInline(admin.TabularInline):
model = Transaction
fields = ['amount', 'member', 'reference', 'text_length_warning', 'ledger']
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
readonly_fields = ['text_length_warning']
extra = 0
def text_length_warning(self, obj):
"""Display reference length, warn if exceeds 140 characters."""
len_reference = len(obj.reference)
len_string = f"{len_reference}/140"
if len_reference > 140:
return mark_safe(f'<span style="color: red;">{len_string}</span>')
return len_string
text_length_warning.short_description = _("Length")
class BillOnSubmittedStatementInline(BillOnStatementInline):
model = BillOnStatementProxy
extra = 0
sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof', 'costs_covered']
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
def get_readonly_fields(self, request, obj=None):
return ['short_description', 'explanation', 'amount', 'paid_by', 'proof']
@admin.register(Transaction) @admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(admin.ModelAdmin):
"""The transaction admin site. This is only used to display transactions. All editing """The transaction admin site. This is only used to display transactions. All editing

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-12 11:37+0200\n" "POT-Creation-Date: 2025-10-16 23:09+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -48,10 +48,6 @@ msgstr "Kostenübersicht"
msgid "Submit statement" msgid "Submit statement"
msgstr "Rechnung einreichen" msgstr "Rechnung einreichen"
#: finance/admin.py
msgid "Length"
msgstr "Länge"
#: finance/admin.py #: finance/admin.py
#, python-format #, python-format
msgid "%(name)s is not yet submitted." msgid "%(name)s is not yet submitted."
@ -180,6 +176,10 @@ msgstr "Bestätigung zurücknehmen"
msgid "Download summary" msgid "Download summary"
msgstr "Beleg herunterladen" msgstr "Beleg herunterladen"
#: finance/admin.py
msgid "Length"
msgstr "Länge"
#: finance/apps.py #: finance/apps.py
msgid "Finance" msgid "Finance"
msgstr "Finanzen" msgstr "Finanzen"
@ -206,7 +206,7 @@ msgid "Submitted"
msgstr "Eingereicht" msgstr "Eingereicht"
#: finance/models.py #: finance/models.py
msgid "Confirmed" msgid "Completed"
msgstr "Abgewickelt" msgstr "Abgewickelt"
#: finance/models.py #: finance/models.py
@ -291,11 +291,6 @@ msgstr "Abrechnung"
msgid "Statements" msgid "Statements"
msgstr "Abrechnungen" msgstr "Abrechnungen"
#: finance/models.py
#, python-format
msgid "Statement: %(excursion)s"
msgstr "Abrechnung: %(excursion)s"
#: finance/models.py #: finance/models.py
#, python-format #, python-format
msgid "Excursion %(excursion)s" msgid "Excursion %(excursion)s"

@ -0,0 +1,28 @@
# Generated by Django 4.2.20 on 2025-10-12 19:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('finance', '0011_remove_statement_confirmed_and_submitted'),
]
operations = [
migrations.CreateModel(
name='StatementOnExcursionProxy',
fields=[
],
options={
'verbose_name': 'Statement',
'verbose_name_plural': 'Statements',
'abstract': False,
'proxy': True,
'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'),
'indexes': [],
'constraints': [],
},
bases=('finance.statement',),
),
]

@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html
from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE
from django.conf import settings from django.conf import settings
import rules import rules
@ -54,11 +55,14 @@ class Statement(CommonModel):
UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2 UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2
STATUS_CHOICES = [(UNSUBMITTED, _('In preparation')), STATUS_CHOICES = [(UNSUBMITTED, _('In preparation')),
(SUBMITTED, _('Submitted')), (SUBMITTED, _('Submitted')),
(CONFIRMED, _('Confirmed'))] (CONFIRMED, _('Completed'))]
STATUS_CSS_CLASS = { SUBMITTED: 'submitted',
CONFIRMED: 'confirmed',
UNSUBMITTED: 'unsubmitted' }
short_description = models.CharField(verbose_name=_('Short description'), short_description = models.CharField(verbose_name=_('Short description'),
max_length=30, max_length=30,
blank=True) blank=False)
explanation = models.TextField(verbose_name=_('Explanation'), blank=True) explanation = models.TextField(verbose_name=_('Explanation'), blank=True)
excursion = models.OneToOneField(Freizeit, verbose_name=_('Associated excursion'), excursion = models.OneToOneField(Freizeit, verbose_name=_('Associated excursion'),
@ -115,20 +119,16 @@ class Statement(CommonModel):
('may_edit_submitted_statements', 'Is allowed to edit submitted statements') ('may_edit_submitted_statements', 'Is allowed to edit submitted statements')
] ]
rules_permissions = { rules_permissions = {
# this is suboptimal, but Statement is only ever used as an inline on Freizeit # All users may add draft statements.
# so we check for excursion permissions 'add_obj': rules.is_staff,
'add_obj': is_leader, # All users may view their own statements and statements of excursions they are responsible for.
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'), 'view_obj': is_creator | leads_excursion | has_global_perm('finance.view_global_statement'),
'change_obj': is_leader & statement_not_submitted, # All users may change relevant (see above) draft statements.
'delete_obj': is_leader & statement_not_submitted, 'change_obj': (not_submitted & (is_creator | leads_excursion)) | has_global_perm('finance.change_global_statement'),
# All users may delete relevant (see above) draft statements.
'delete_obj': not_submitted & (is_creator | leads_excursion | has_global_perm('finance.delete_global_statement')),
} }
def __str__(self):
if self.excursion is not None:
return _('Statement: %(excursion)s') % {'excursion': str(self.excursion)}
else:
return self.short_description
@property @property
def title(self): def title(self):
if self.excursion is not None: if self.excursion is not None:
@ -136,6 +136,9 @@ class Statement(CommonModel):
else: else:
return self.short_description return self.short_description
def __str__(self):
return str(self.title)
@property @property
def submitted(self): def submitted(self):
return self.status == Statement.SUBMITTED or self.status == Statement.CONFIRMED return self.status == Statement.SUBMITTED or self.status == Statement.CONFIRMED
@ -144,6 +147,13 @@ class Statement(CommonModel):
def confirmed(self): def confirmed(self):
return self.status == Statement.CONFIRMED return self.status == Statement.CONFIRMED
def status_badge(self):
code = Statement.STATUS_CSS_CLASS[self.status]
return format_html(f'<span class="statement-{code}">{Statement.STATUS_CHOICES[self.status][1]}</span>')
status_badge.short_description = _('Status')
status_badge.allow_tags = True
status_badge.admin_order_field = 'status'
def submit(self, submitter=None): def submit(self, submitter=None):
self.status = self.SUBMITTED self.status = self.SUBMITTED
self.submitted_date = timezone.now() self.submitted_date = timezone.now()
@ -509,6 +519,7 @@ class Statement(CommonModel):
def total_pretty(self): def total_pretty(self):
return "{}".format(self.total) return "{}".format(self.total)
total_pretty.short_description = _('Total') total_pretty.short_description = _('Total')
total_pretty.admin_order_field = 'total'
def template_context(self): def template_context(self):
context = { context = {
@ -580,6 +591,20 @@ class Statement(CommonModel):
attachments=[media_path(filename)]) attachments=[media_path(filename)])
class StatementOnExcursionProxy(Statement):
class Meta(CommonModel.Meta):
proxy = True
verbose_name = _('Statement')
verbose_name_plural = _('Statements')
rules_permissions = {
# This is used as an inline on excursions, so we check for excursion permissions.
'add_obj': is_leader,
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'),
'change_obj': is_leader & statement_not_submitted,
'delete_obj': is_leader & statement_not_submitted,
}
class StatementUnSubmittedManager(models.Manager): class StatementUnSubmittedManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(status=Statement.UNSUBMITTED) return super().get_queryset().filter(status=Statement.UNSUBMITTED)
@ -636,7 +661,7 @@ class StatementConfirmed(Statement):
class Bill(CommonModel): class Bill(CommonModel):
statement = models.ForeignKey(Statement, verbose_name=_('Statement'), on_delete=models.CASCADE) statement = models.ForeignKey(Statement, verbose_name=_('Statement'), on_delete=models.CASCADE)
short_description = models.CharField(verbose_name=_('Short description'), max_length=30) short_description = models.CharField(verbose_name=_('Short description'), max_length=30, blank=False)
explanation = models.TextField(verbose_name=_('Explanation'), blank=True) explanation = models.TextField(verbose_name=_('Explanation'), blank=True)
amount = models.DecimalField(verbose_name=_('Amount'), max_digits=6, decimal_places=2, default=0) amount = models.DecimalField(verbose_name=_('Amount'), max_digits=6, decimal_places=2, default=0)

@ -4,6 +4,7 @@ from django.test import TestCase, override_settings
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.test import RequestFactory, Client from django.test import RequestFactory, Client
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.contrib.auth import models as authmodels
from django.utils import timezone from django.utils import timezone
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.middleware import MessageMiddleware
@ -24,8 +25,7 @@ from ..models import (
StatementSubmitted StatementSubmitted
) )
from ..admin import ( from ..admin import (
LedgerAdmin, StatementUnSubmittedAdmin, StatementSubmittedAdmin, LedgerAdmin, StatementAdmin, TransactionAdmin, BillAdmin
StatementConfirmedAdmin, TransactionAdmin, BillAdmin
) )
@ -52,10 +52,10 @@ class AdminTestCase(TestCase):
class StatementUnSubmittedAdminTestCase(AdminTestCase): class StatementUnSubmittedAdminTestCase(AdminTestCase):
"""Test cases for StatementUnSubmittedAdmin""" """Test cases for StatementAdmin in the case of unsubmitted statements"""
def setUp(self): def setUp(self):
super().setUp(model=StatementUnSubmitted, admin=StatementUnSubmittedAdmin) super().setUp(model=Statement, admin=StatementAdmin)
self.superuser = User.objects.get(username='superuser') self.superuser = User.objects.get(username='superuser')
self.member = Member.objects.create( self.member = Member.objects.create(
@ -96,6 +96,26 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase):
self.admin.save_model(request, new_statement, None, change=False) self.admin.save_model(request, new_statement, None, change=False)
self.assertEqual(new_statement.created_by, self.member) self.assertEqual(new_statement.created_by, self.member)
def test_has_delete_permission(self):
"""Test if unsubmitted statements may be deleted"""
request = self.factory.post('/')
request.user = self.superuser
self.assertTrue(self.admin.has_delete_permission(request, self.statement))
def test_get_fields(self):
"""Test get_fields when excursion is set or not set."""
request = self.factory.post('/')
request.user = self.superuser
self.assertIn('excursion', self.admin.get_fields(request, self.statement_with_excursion))
self.assertNotIn('excursion', self.admin.get_fields(request, self.statement))
self.assertNotIn('excursion', self.admin.get_fields(request))
def test_get_inlines(self):
"""Test get_inlines"""
request = self.factory.post('/')
request.user = self.superuser
self.assertEqual(len(self.admin.get_inlines(request, self.statement)), 1)
def test_get_readonly_fields_submitted(self): def test_get_readonly_fields_submitted(self):
"""Test readonly fields when statement is submitted""" """Test readonly fields when statement is submitted"""
# Mark statement as submitted # Mark statement as submitted
@ -111,7 +131,7 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase):
self.assertEqual(readonly_fields, ['status', 'excursion']) self.assertEqual(readonly_fields, ['status', 'excursion'])
def test_submit_view_insufficient_permission(self): def test_submit_view_insufficient_permission(self):
url = reverse('admin:finance_statementunsubmitted_submit', url = reverse('admin:finance_statement_submit',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('standard') c = self._login('standard')
response = c.get(url, follow=True) response = c.get(url, follow=True)
@ -119,7 +139,7 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase):
self.assertContains(response, _('Insufficient permissions.')) self.assertContains(response, _('Insufficient permissions.'))
def test_submit_view_get(self): def test_submit_view_get(self):
url = reverse('admin:finance_statementunsubmitted_submit', url = reverse('admin:finance_statement_submit',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url, follow=True) response = c.get(url, follow=True)
@ -127,7 +147,7 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase):
self.assertContains(response, _('Submit statement')) self.assertContains(response, _('Submit statement'))
def test_submit_view_get_with_excursion(self): def test_submit_view_get_with_excursion(self):
url = reverse('admin:finance_statementunsubmitted_submit', url = reverse('admin:finance_statement_submit',
args=(self.statement_with_excursion.pk,)) args=(self.statement_with_excursion.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url, follow=True) response = c.get(url, follow=True)
@ -135,7 +155,7 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase):
self.assertContains(response, _('Finance overview')) self.assertContains(response, _('Finance overview'))
def test_submit_view_post(self): def test_submit_view_post(self):
url = reverse('admin:finance_statementunsubmitted_submit', url = reverse('admin:finance_statement_submit',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'apply': ''}) response = c.post(url, follow=True, data={'apply': ''})
@ -145,10 +165,10 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase):
class StatementSubmittedAdminTestCase(AdminTestCase): class StatementSubmittedAdminTestCase(AdminTestCase):
"""Test cases for StatementSubmittedAdmin""" """Test cases for StatementAdmin in the case of submitted statements"""
def setUp(self): def setUp(self):
super().setUp(model=StatementSubmitted, admin=StatementSubmittedAdmin) super().setUp(model=Statement, admin=StatementAdmin)
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass') self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.member = Member.objects.create( self.member = Member.objects.create(
@ -157,8 +177,8 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
) )
self.finance_user = User.objects.create_user('finance', 'finance@example.com', 'pass') self.finance_user = User.objects.create_user('finance', 'finance@example.com', 'pass')
finance_perm = Permission.objects.get(codename='process_statementsubmitted') self.finance_user.groups.add(authmodels.Group.objects.get(name='Finance'),
self.finance_user.user_permissions.add(finance_perm) authmodels.Group.objects.get(name='Standard'))
self.statement = Statement.objects.create( self.statement = Statement.objects.create(
short_description='Submitted Statement', short_description='Submitted Statement',
@ -247,12 +267,6 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
paid_by=self.member paid_by=self.member
) )
def test_has_add_permission(self):
"""Test that add permission is disabled"""
request = self.factory.get('/')
request.user = self.finance_user
self.assertFalse(self.admin.has_add_permission(request))
def test_has_change_permission_with_permission(self): def test_has_change_permission_with_permission(self):
"""Test change permission with proper permission""" """Test change permission with proper permission"""
request = self.factory.get('/') request = self.factory.get('/')
@ -276,14 +290,14 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
self.admin.get_readonly_fields(None, self.statement_unsubmitted)) self.admin.get_readonly_fields(None, self.statement_unsubmitted))
def test_change(self): def test_change(self):
url = reverse('admin:finance_statementsubmitted_change', url = reverse('admin:finance_statement_change',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url) response = c.get(url)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
def test_overview_view(self): def test_overview_view(self):
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url) response = c.get(url)
@ -297,7 +311,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
self.statement.status = Statement.UNSUBMITTED self.statement.status = Statement.UNSUBMITTED
self.statement.save() self.statement.save()
url = reverse('admin:finance_statementsubmitted_overview', args=(self.statement.pk,)) url = reverse('admin:finance_statement_overview', args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url, follow=True) response = c.get(url, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
@ -314,7 +328,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Create a bill that matches the transaction amount to make it valid # Create a bill that matches the transaction amount to make it valid
self._create_matching_bill() self._create_matching_bill()
url = reverse('admin:finance_statementsubmitted_overview', args=(self.statement.pk,)) url = reverse('admin:finance_statement_overview', args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'transaction_execution_confirm': ''}) response = c.post(url, follow=True, data={'transaction_execution_confirm': ''})
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
@ -332,7 +346,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Create a bill that matches the transaction amount to make it valid # Create a bill that matches the transaction amount to make it valid
self._create_matching_bill() self._create_matching_bill()
url = reverse('admin:finance_statementsubmitted_overview', args=(self.statement.pk,)) url = reverse('admin:finance_statement_overview', args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'transaction_execution_confirm_and_send': ''}) response = c.post(url, follow=True, data={'transaction_execution_confirm_and_send': ''})
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
@ -349,7 +363,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Create a bill that matches the transaction amount to make total valid # Create a bill that matches the transaction amount to make total valid
self._create_matching_bill() self._create_matching_bill()
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, data={'confirm': ''}) response = c.post(url, data={'confirm': ''})
@ -361,7 +375,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Create a bill that doesn't match the transaction # Create a bill that doesn't match the transaction
self._create_non_matching_bill() self._create_non_matching_bill()
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'confirm': ''}) response = c.post(url, follow=True, data={'confirm': ''})
@ -378,7 +392,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Create a bill that matches the transaction amount to pass the first check # Create a bill that matches the transaction amount to pass the first check
self._create_matching_bill() self._create_matching_bill()
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'confirm': ''}) response = c.post(url, follow=True, data={'confirm': ''})
@ -406,7 +420,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Check validity obstruction is allowances # Check validity obstruction is allowances
self.assertEqual(self.statement_no_trans_success.validity, Statement.INVALID_ALLOWANCE_TO) self.assertEqual(self.statement_no_trans_success.validity, Statement.INVALID_ALLOWANCE_TO)
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement_no_trans_success.pk,)) args=(self.statement_no_trans_success.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'confirm': ''}) response = c.post(url, follow=True, data={'confirm': ''})
@ -418,7 +432,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
def test_overview_view_reject(self): def test_overview_view_reject(self):
"""Test overview_view reject statement""" """Test overview_view reject statement"""
url = reverse('admin:finance_statementsubmitted_overview', args=(self.statement.pk,)) url = reverse('admin:finance_statement_overview', args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'reject': ''}) response = c.post(url, follow=True, data={'reject': ''})
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
@ -435,7 +449,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
# Ensure there's already a transaction # Ensure there's already a transaction
self.assertTrue(self.statement.transaction_set.count() > 0) self.assertTrue(self.statement.transaction_set.count() > 0)
url = reverse('admin:finance_statementsubmitted_overview', args=(self.statement.pk,)) url = reverse('admin:finance_statement_overview', args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'generate_transactions': ''}) response = c.post(url, follow=True, data={'generate_transactions': ''})
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
@ -444,7 +458,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
def test_overview_view_generate_transactions_success(self): def test_overview_view_generate_transactions_success(self):
"""Test overview_view generate transactions successfully""" """Test overview_view generate transactions successfully"""
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement_no_trans_success.pk,)) args=(self.statement_no_trans_success.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'generate_transactions': ''}) response = c.post(url, follow=True, data={'generate_transactions': ''})
@ -455,7 +469,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
def test_overview_view_generate_transactions_error(self): def test_overview_view_generate_transactions_error(self):
"""Test overview_view generate transactions with error""" """Test overview_view generate transactions with error"""
url = reverse('admin:finance_statementsubmitted_overview', url = reverse('admin:finance_statement_overview',
args=(self.statement_no_trans_error.pk,)) args=(self.statement_no_trans_error.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, follow=True, data={'generate_transactions': ''}) response = c.post(url, follow=True, data={'generate_transactions': ''})
@ -466,10 +480,10 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
self.assertTrue(any(expected_text in str(msg) for msg in messages)) self.assertTrue(any(expected_text in str(msg) for msg in messages))
def test_reduce_transactions_view(self): def test_reduce_transactions_view(self):
url = reverse('admin:finance_statementsubmitted_reduce_transactions', url = reverse('admin:finance_statement_reduce_transactions',
args=(self.statement.pk,)) args=(self.statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url, data={'redirectTo': reverse('admin:finance_statementsubmitted_changelist')}, response = c.get(url, data={'redirectTo': reverse('admin:finance_statement_changelist')},
follow=True) follow=True)
self.assertContains(response, self.assertContains(response,
_("Successfully reduced transactions for %(name)s.") %\ _("Successfully reduced transactions for %(name)s.") %\
@ -477,10 +491,10 @@ class StatementSubmittedAdminTestCase(AdminTestCase):
class StatementConfirmedAdminTestCase(AdminTestCase): class StatementConfirmedAdminTestCase(AdminTestCase):
"""Test cases for StatementConfirmedAdmin""" """Test cases for StatementAdmin in the case of confirmed statements"""
def setUp(self): def setUp(self):
super().setUp(model=StatementConfirmed, admin=StatementConfirmedAdmin) super().setUp(model=Statement, admin=StatementAdmin)
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass') self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.member = Member.objects.create( self.member = Member.objects.create(
@ -489,8 +503,8 @@ class StatementConfirmedAdminTestCase(AdminTestCase):
) )
self.finance_user = User.objects.create_user('finance', 'finance@example.com', 'pass') self.finance_user = User.objects.create_user('finance', 'finance@example.com', 'pass')
unconfirm_perm = Permission.objects.get(codename='may_manage_confirmed_statements') self.finance_user.groups.add(authmodels.Group.objects.get(name='Finance'),
self.finance_user.user_permissions.add(unconfirm_perm) authmodels.Group.objects.get(name='Standard'))
# Create a base statement first # Create a base statement first
base_statement = Statement.objects.create( base_statement = Statement.objects.create(
@ -544,23 +558,17 @@ class StatementConfirmedAdminTestCase(AdminTestCase):
middleware.process_request(request) middleware.process_request(request)
request._messages = FallbackStorage(request) request._messages = FallbackStorage(request)
def test_has_add_permission(self):
"""Test that add permission is disabled"""
request = self.factory.get('/')
request.user = self.finance_user
self.assertFalse(self.admin.has_add_permission(request))
def test_has_change_permission(self): def test_has_change_permission(self):
"""Test that change permission is disabled""" """Test that change permission is disabled"""
request = self.factory.get('/') request = self.factory.get('/')
request.user = self.finance_user request.user = self.finance_user
self.assertFalse(self.admin.has_change_permission(request)) self.assertFalse(self.admin.has_change_permission(request, self.statement))
def test_has_delete_permission(self): def test_has_delete_permission(self):
"""Test that delete permission is disabled""" """Test that delete permission is disabled"""
request = self.factory.get('/') request = self.factory.get('/')
request.user = self.finance_user request.user = self.finance_user
self.assertFalse(self.admin.has_delete_permission(request)) self.assertFalse(self.admin.has_delete_permission(request, self.statement))
def test_unconfirm_view_not_confirmed_statement(self): def test_unconfirm_view_not_confirmed_statement(self):
"""Test unconfirm_view with statement that is not confirmed""" """Test unconfirm_view with statement that is not confirmed"""
@ -622,14 +630,15 @@ class StatementConfirmedAdminTestCase(AdminTestCase):
self.assertIn(self.statement.short_description.encode(), response.content) self.assertIn(self.statement.short_description.encode(), response.content)
def test_statement_summary_view_insufficient_permission(self): def test_statement_summary_view_insufficient_permission(self):
url = reverse('admin:finance_statementconfirmed_summary', url = reverse('admin:finance_statement_summary',
args=(self.statement_with_excursion.pk,)) args=(self.statement_with_excursion.pk,))
c = self._login('standard') c = self._login('standard')
response = c.get(url, follow=True) response = c.get(url, follow=True)
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Insufficient permissions.'))
def test_statement_summary_view_unconfirmed(self): def test_statement_summary_view_unconfirmed(self):
url = reverse('admin:finance_statementconfirmed_summary', url = reverse('admin:finance_statement_summary',
args=(self.unconfirmed_statement.pk,)) args=(self.unconfirmed_statement.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url, follow=True) response = c.get(url, follow=True)
@ -638,7 +647,7 @@ class StatementConfirmedAdminTestCase(AdminTestCase):
def test_statement_summary_view_confirmed_with_excursion(self): def test_statement_summary_view_confirmed_with_excursion(self):
"""Test statement_summary_view when statement is confirmed with excursion""" """Test statement_summary_view when statement is confirmed with excursion"""
url = reverse('admin:finance_statementconfirmed_summary', url = reverse('admin:finance_statement_summary',
args=(self.statement_with_excursion.pk,)) args=(self.statement_with_excursion.pk,))
c = self._login('superuser') c = self._login('superuser')
response = c.get(url, follow=True) response = c.get(url, follow=True)

@ -26,9 +26,7 @@ JET_SIDE_MENU_ITEMS = [
{'name': 'emailaddress', 'permissions': ['mailer.view_emailaddress']}, {'name': 'emailaddress', 'permissions': ['mailer.view_emailaddress']},
]}, ]},
{'app_label': 'finance', 'items': [ {'app_label': 'finance', 'items': [
{'name': 'statementunsubmitted', 'permissions': ['finance.view_statementunsubmitted']}, {'name': 'statement', 'permissions': ['finance.view_statement']},
{'name': 'statementsubmitted', 'permissions': ['finance.view_statementsubmitted']},
{'name': 'statementconfirmed', 'permissions': ['finance.view_statementconfirmed']},
{'name': 'ledger', 'permissions': ['finance.view_ledger']}, {'name': 'ledger', 'permissions': ['finance.view_ledger']},
{'name': 'bill', 'permissions': ['finance.view_bill', 'finance.view_bill_admin']}, {'name': 'bill', 'permissions': ['finance.view_bill', 'finance.view_bill_admin']},
{'name': 'transaction', 'permissions': ['finance.view_transaction']}, {'name': 'transaction', 'permissions': ['finance.view_transaction']},

@ -42,7 +42,7 @@ from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, K
KlettertreffAttendee, ActivityCategory, EmergencyContact, KlettertreffAttendee, ActivityCategory, EmergencyContact,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy, annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy,
InvitationToGroup) InvitationToGroup)
from finance.models import Statement, BillOnExcursionProxy from finance.models import BillOnExcursionProxy, StatementOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings from django.conf import settings
from utils import get_member, RestrictedFileField, mondays_until_nth from utils import get_member, RestrictedFileField, mondays_until_nth
@ -941,7 +941,7 @@ class StatementOnListForm(forms.ModelForm):
self.fields['ljp_to'].queryset = excursion.jugendleiter.all() self.fields['ljp_to'].queryset = excursion.jugendleiter.all()
class Meta: class Meta:
model = Statement model = StatementOnExcursionProxy
fields = ['night_cost', 'allowance_to', 'subsidy_to', 'ljp_to'] fields = ['night_cost', 'allowance_to', 'subsidy_to', 'ljp_to']
def clean(self): def clean(self):
@ -959,7 +959,7 @@ class StatementOnListForm(forms.ModelForm):
class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = Statement model = StatementOnExcursionProxy
extra = 1 extra = 1
description = _('Please list here all expenses in relation with this excursion and upload relevant bills. These have to be permanently stored for the application of LJP contributions. The short descriptions are used in the seminar report cost overview (possible descriptions are e.g. food, material, etc.).') description = _('Please list here all expenses in relation with this excursion and upload relevant bills. These have to be permanently stored for the application of LJP contributions. The short descriptions are used in the seminar report cost overview (possible descriptions are e.g. food, material, etc.).')
sortable_options = [] sortable_options = []

@ -0,0 +1,121 @@
from django.utils.translation import gettext_lazy as _
from django.db import migrations
from django.contrib.auth.management import create_permissions
STANDARD_PERMS = [
('members', 'view_member'),
('members', 'view_freizeit'),
('members', 'add_global_freizeit'),
('members', 'view_memberwaitinglist'),
('members', 'view_memberunconfirmedproxy'),
('mailer', 'view_message'),
('mailer', 'add_global_message'),
('finance', 'view_statement'),
('finance', 'add_global_statement'),
]
FINANCE_PERMS = [
('finance', 'view_bill'),
('finance', 'view_ledger'),
('finance', 'add_ledger'),
('finance', 'change_ledger'),
('finance', 'delete_ledger'),
('finance', 'view_global_statement'),
('finance', 'change_global_statement'),
('finance', 'process_statementsubmitted'),
('finance', 'may_manage_confirmed_statements'),
('finance', 'view_transaction'),
('finance', 'change_transaction'),
('finance', 'add_transaction'),
('finance', 'delete_transaction'),
('members', 'list_global_freizeit'),
('members', 'view_global_freizeit'),
]
WAITINGLIST_PERMS = [
('members', 'view_global_memberwaitinglist'),
('members', 'list_global_memberwaitinglist'),
('members', 'change_global_memberwaitinglist'),
('members', 'delete_global_memberwaitinglist'),
]
TRAINING_PERMS = [
('members', 'change_global_member'),
('members', 'list_global_member'),
('members', 'view_global_member'),
('members', 'add_global_membertraining'),
('members', 'change_global_membertraining'),
('members', 'list_global_membertraining'),
('members', 'view_global_membertraining'),
('members', 'view_trainingcategory'),
('members', 'add_trainingcategory'),
('members', 'change_trainingcategory'),
('members', 'delete_trainingcategory'),
]
REGISTRATION_PERMS = [
('members', 'may_manage_all_registrations'),
('members', 'change_memberunconfirmedproxy'),
('members', 'delete_memberunconfirmedproxy'),
]
MATERIAL_PERMS = [
('members', 'list_global_member'),
('material', 'view_materialpart'),
('material', 'change_materialpart'),
('material', 'add_materialpart'),
('material', 'delete_materialpart'),
('material', 'view_materialcategory'),
('material', 'change_materialcategory'),
('material', 'add_materialcategory'),
('material', 'delete_materialcategory'),
('material', 'view_ownership'),
('material', 'change_ownership'),
('material', 'add_ownership'),
('material', 'delete_ownership'),
]
def ensure_group_perms(apps, schema_editor, name, perm_names):
"""
Ensure the group `name` has the permissions `perm_names`. If the group does not
exist, create it with the given permissions, otherwise add the missing ones.
This only adds permissions, already existing ones that are not listed here are not
removed.
"""
db_alias = schema_editor.connection.alias
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
perms = [ Permission.objects.get(codename=codename, content_type__app_label=app_label) for app_label, codename in perm_names ]
try:
g = Group.objects.using(db_alias).get(name=name)
for perm in perms:
g.permissions.add(perm)
g.save()
# This case is only executed if users have manually removed one of the standard groups.
except Group.DoesNotExist: # pragma: no cover
g = Group.objects.using(db_alias).create(name=name)
g.permissions.set(perms)
g.save()
def update_default_permission_groups(apps, schema_editor):
ensure_group_perms(apps, schema_editor, "Standard", STANDARD_PERMS)
ensure_group_perms(apps, schema_editor, "Finance", FINANCE_PERMS)
ensure_group_perms(apps, schema_editor, "Waitinglist", WAITINGLIST_PERMS)
ensure_group_perms(apps, schema_editor, "Trainings", TRAINING_PERMS)
ensure_group_perms(apps, schema_editor, "Registrations", REGISTRATION_PERMS)
ensure_group_perms(apps, schema_editor, "Material", MATERIAL_PERMS)
class Migration(migrations.Migration):
dependencies = [
('finance', '0012_statementonexcursionproxy'),
('members', '0044_membertraining_activity_and_more'),
]
operations = [
migrations.RunPython(update_default_permission_groups, migrations.RunPython.noop),
]

@ -652,7 +652,9 @@ class Member(Person):
elif name == "NewMemberOnList": elif name == "NewMemberOnList":
return queryset return queryset
elif name == "Statement": elif name == "Statement":
return queryset return self.filter_statements_by_permissions(queryset, annotate)
elif name == "StatementOnExcursionProxy":
return self.filter_statements_by_permissions(queryset, annotate)
elif name == "BillOnExcursionProxy": elif name == "BillOnExcursionProxy":
return queryset return queryset
elif name == "Intervention": elif name == "Intervention":

@ -0,0 +1,25 @@
span.statement-unsubmitted, span.statement-submitted, span.statement-confirmed {
color: black;
padding: 4px;
padding-left: 6px;
padding-right: 6px;
border-radius: 10px;
width: 20px;
min-width: 20px;
max-width: 20px;
}
span.statement-submitted {
background-color: #e8e8bd;
color: black;
}
span.statement-unsubmitted {
background-color: #f0dada;
color: black;
}
span.statement-confirmed {
background-color: #e0eec5;
color: black;
}

@ -14,6 +14,7 @@
<link rel="stylesheet" type="text/css" href="{% static "jet/css/themes/"|add:THEME|add:"/base.css" as url %}{{ url|jet_append_version }}" class="base-stylesheet" /> <link rel="stylesheet" type="text/css" href="{% static "jet/css/themes/"|add:THEME|add:"/base.css" as url %}{{ url|jet_append_version }}" class="base-stylesheet" />
<link rel="stylesheet" type="text/css" href="{% static "jet/css/themes/"|add:THEME|add:"/select2.theme.css" as url %}{{ url|jet_append_version }}" class="select2-stylesheet" /> <link rel="stylesheet" type="text/css" href="{% static "jet/css/themes/"|add:THEME|add:"/select2.theme.css" as url %}{{ url|jet_append_version }}" class="select2-stylesheet" />
<link rel="stylesheet" type="text/css" href="{% static "jet/css/themes/"|add:THEME|add:"/jquery-ui.theme.css" as url %}{{ url|jet_append_version }}" class="jquery-ui-stylesheet" /> <link rel="stylesheet" type="text/css" href="{% static "jet/css/themes/"|add:THEME|add:"/jquery-ui.theme.css" as url %}{{ url|jet_append_version }}" class="jquery-ui-stylesheet" />
<link rel="stylesheet" type="text/css" href="{% static "admin/css/extra.css" %}">
{% block extrastyle %}{% endblock %} {% block extrastyle %}{% endblock %}
{% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %} {% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %}

@ -0,0 +1,41 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls rules %}
{% block object-tools-items %}
{% if original.confirmed and perms.finance.may_manage_confirmed_statements %}
<li>
{% url opts|admin_urlname:'unconfirm' original.pk|admin_urlquote as invite_url %}
<a class="historylink" href="{% add_preserved_filters invite_url %}">{% trans 'Unconfirm' %}</a>
</li>
<li>
{% url opts|admin_urlname:'summary' original.pk|admin_urlquote as invite_url %}
<a class="historylink" target="_blank" href="{% add_preserved_filters invite_url %}">{% trans 'Download summary' %}</a>
</li>
{% elif original.submitted and perms.finance.process_statementsubmitted %}
<script>
function requestWithCurrentURL(path) {
var xpath = path + "?redirectTo=" + window.location.href;
location.href = xpath;
}
</script>
<li>
{% url opts|admin_urlname:'reduce_transactions' original.pk|admin_urlquote as invite_url %}
<a value="hi" onclick='requestWithCurrentURL("{% add_preserved_filters invite_url %}")'>
{% trans 'Reduce transactions' %}
</a>
</li>
<li>
{% url opts|admin_urlname:'overview' original.pk|admin_urlquote as invite_url %}
<a class="historylink" href="{% add_preserved_filters invite_url %}">{% trans 'Overview' %}</a>
</li>
{% elif not original.submitted %}
<li>
{% url opts|admin_urlname:'submit' original.pk|admin_urlquote as invite_url %}
<a class="historylink" href="{% add_preserved_filters invite_url %}">{% trans 'Submit' %}</a>
</li>
{% endif %}
{{block.super}}
{% endblock %}

@ -24,7 +24,7 @@ for (var i = 0; i < els.length; ++i) {
Der KOMPASS ist dein Kompass in der Jugendarbeit der JDAV {% settings_value 'SEKTION' %}. Hier hast du Zugriff Der KOMPASS ist dein Kompass in der Jugendarbeit der JDAV {% settings_value 'SEKTION' %}. Hier hast du Zugriff
auf deine Jugendgruppen, deine letzten auf deine Jugendgruppen, deine letzten
<a href="{% url 'admin:members_freizeit_changelist' %}">Ausfahrten</a> und deine <a href="{% url 'admin:members_freizeit_changelist' %}">Ausfahrten</a> und deine
<a href="{% url 'admin:finance_statementunsubmitted_changelist' %}">Abrechnungen</a>. <a href="{% url 'admin:finance_statement_changelist' %}">Abrechnungen</a>.
</p> </p>
</div> </div>

Loading…
Cancel
Save