diff --git a/jdav_web/finance/__init__.py b/jdav_web/finance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py new file mode 100644 index 0000000..184b9d2 --- /dev/null +++ b/jdav_web/finance/admin.py @@ -0,0 +1,152 @@ +from django.contrib import admin, messages +from django.forms import Textarea +from django.http import HttpResponse, HttpResponseRedirect +from django.db.models import TextField +from django.urls import path, reverse +from functools import update_wrapper +from django.utils.translation import gettext_lazy as _ +from django.shortcuts import render + +from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted + +@admin.register(Ledger) +class LedgerAdmin(admin.ModelAdmin): + pass + + +class BillOnStatementInline(admin.TabularInline): + model = Bill + extra = 0 + sortable_options = [] + fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof'] + formfield_overrides = { + TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} + } + + def get_readonly_fields(self, request, obj=None): + if obj is not None and obj.submitted: + return self.fields + return super(BillOnStatementInline, self).get_readonly_fields(request, obj) + + +@admin.register(Statement) +class StatementAdmin(admin.ModelAdmin): + fields = ['short_description', 'explanation', 'excursion', 'submitted'] + inlines = [BillOnStatementInline] + + def get_readonly_fields(self, request, obj=None): + readonly_fields = ['submitted'] + 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( + "/submit/", + wrap(self.submit_view), + name="%s_%s_submit" % (self.opts.app_label, self.opts.model_name), + ), + ] + return custom_urls + urls + + def submit_view(self, request, object_id): + statement = Statement.objects.get(pk=object_id) + if statement.submitted: + messages.error(request, + _("%(name)s is already submitted.") % {'name': str(statement)}) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (statement._meta.app_label, statement._meta.model_name), args=(statement.pk,))) + + if "apply" in request.POST: + statement.submit() + messages.success(request, + _("Successfully submited %(name)s. The finance department will notify the requestors as soon as possible.") % {'name': str(statement)}) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (statement._meta.app_label, statement._meta.model_name), args=(statement.pk,))) + context = dict(self.admin_site.each_context(request), + title=_('Submit statement'), + opts=self.opts, + statement=statement) + + return render(request, 'admin/submit_statement.html', context=context) + + +@admin.register(StatementSubmitted) +class StatementSubmittedAdmin(admin.ModelAdmin): + fields = ['short_description', 'explanation', 'excursion', 'submitted'] + inlines = [BillOnStatementInline] + + def has_add_permission(self, request, obj=None): + return False + + def get_readonly_fields(self, request, obj=None): + readonly_fields = ['submitted'] + 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( + "/overview/", + wrap(self.overview_view), + name="%s_%s_overview" % (self.opts.app_label, self.opts.model_name), + ), + ] + return custom_urls + urls + + def overview_view(self, request, object_id): + statement = Statement.objects.get(pk=object_id) + if not statement.submitted: + messages.error(request, + _("%(name)s is not yet submitted.") % {'name': str(statement)}) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) + + if "apply" in request.POST: + #statement.submit() + #messages.success(request, + # _("Successfully submited %(name)s. The finance department will notify the requestors as soon as possible.") % {'name': str(statement)}) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) + context = dict(self.admin_site.each_context(request), + title=_('View submitted statement'), + opts=self.opts, + statement=statement, + total_bills=statement.total_bills(), + total_transportation=statement.total_transportation(), + total=statement.total()) + + return render(request, 'admin/overview_submitted_statement.html', context=context) + + +@admin.register(Receipt) +class ReceiptAdmin(admin.ModelAdmin): + pass + + +@admin.register(Transaction) +class TransactionAdmin(admin.ModelAdmin): + pass + + +@admin.register(Bill) +class BillAdmin(admin.ModelAdmin): + list_display = ['short_description', 'pretty_amount', 'paid_by', 'refunded'] diff --git a/jdav_web/finance/apps.py b/jdav_web/finance/apps.py new file mode 100644 index 0000000..53023d8 --- /dev/null +++ b/jdav_web/finance/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FinanceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'finance' diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py new file mode 100644 index 0000000..7f16ac8 --- /dev/null +++ b/jdav_web/finance/models.py @@ -0,0 +1,105 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from members.models import Member, Freizeit + +# Create your models here. + +class Ledger(models.Model): + name = models.CharField(verbose_name=_('Name'), max_length=30) + + def __str__(self): + return self.name + + +class Statement(models.Model): + short_description = models.CharField(verbose_name=_('Short description'), + max_length=30, + blank=True) + explanation = models.TextField(verbose_name=_('Explanation'), blank=True) + + excursion = models.OneToOneField(Freizeit, verbose_name=_('Associated excursion'), + blank=True, + null=True, + on_delete=models.SET_NULL) + + submitted = models.BooleanField(verbose_name=_('Submitted'), default=False) + + class Meta: + permissions = [('may_edit_submitted_statements', 'Is allowed to edit submitted statements')] + + def __str__(self): + if self.excursion is not None: + return _('Statement: %(excursion)s') % {'excursion': str(self.excursion)} + else: + return self.short_description + + def submit(self): + self.submitted = True + self.save() + + def total_bills(self): + return sum([bill.amount for bill in self.bill_set.all()]) + + def total_transportation(self): + if self.excursion is None: + return 0 + + exc = self.excursion + return exc.kilometers_traveled * 0.2 + + def total(self): + return float(self.total_bills()) + self.total_transportation() + + +class StatementSubmittedManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(submitted=True) + + +class StatementSubmitted(Statement): + objects = StatementSubmittedManager() + + class Meta: + proxy = True + verbose_name = _('Submitted statement') + verbose_name_plural = _('Submitted statements') + permissions = (('may_manage_submitted_statements', 'Can view and manage submitted statements.'),) + +class Bill(models.Model): + statement = models.ForeignKey(Statement, verbose_name=_('Statement'), + on_delete=models.CASCADE) + short_description = models.CharField(verbose_name=_('Short description'), max_length=30) + explanation = models.TextField(verbose_name=_('Explanation'), blank=True) + + amount = models.DecimalField(max_digits=6, decimal_places=2, default=0) + paid_by = models.ForeignKey(Member, verbose_name=_('Paid by'), null=True, + on_delete=models.SET_NULL) + refunded = models.BooleanField(verbose_name=_('Refunded'), default=False) + + proof = models.ImageField(_('Proof'), upload_to='bill_images', blank=True) + + def __str__(self): + return "{} ({}€)".format(self.short_description, self.amount) + + def pretty_amount(self): + return "{} €".format(self.amount) + pretty_amount.admin_order_field = 'amount' + + +class Transaction(models.Model): + amount = models.DecimalField(max_digits=6, decimal_places=2) + member = models.ForeignKey(Member, verbose_name=_('Recipient'), + on_delete=models.CASCADE) + + statement = models.ForeignKey(Statement, verbose_name=_('Statement'), + on_delete=models.CASCADE) + + confirmed = models.BooleanField(verbose_name=_('Confirmed')) + + +class Receipt(models.Model): + short_description = models.CharField(verbose_name=_('Short description'), max_length=30) + ledger = models.ForeignKey(Ledger, blank=False, null=False, verbose_name=_('Ledger'), + on_delete=models.CASCADE) + amount = models.DecimalField(max_digits=6, decimal_places=2) + comments = models.TextField() diff --git a/jdav_web/finance/templates/admin/overview_submitted_statement.html b/jdav_web/finance/templates/admin/overview_submitted_statement.html new file mode 100644 index 0000000..6572822 --- /dev/null +++ b/jdav_web/finance/templates/admin/overview_submitted_statement.html @@ -0,0 +1,55 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static %} + +{% block extrahead %} + {{ block.super }} + {{ media }} + + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% translate "Overview" %}

+ + + +

{% blocktrans %}The total amount is {{total_bills}} €.{% endblocktrans %}

+ +{% if statement.excursion %} + +

{% trans "Excursion" %}

+

{% blocktrans %}Total distance traveled: {{ statement.excursion.kilometers_traveled }} km by +{{ statement.excursion.tour_approach }}. This results in {{ total_transportation }} €.{% endblocktrans %} +

+ +{% endif %} + +{% blocktrans %} This results in a total amount of {{ total }} € {% endblocktrans %} + +
+ {% csrf_token %} + + + {% translate "Cancel" %} +
+{% endblock %} diff --git a/jdav_web/finance/templates/admin/submit_statement.html b/jdav_web/finance/templates/admin/submit_statement.html new file mode 100644 index 0000000..838c410 --- /dev/null +++ b/jdav_web/finance/templates/admin/submit_statement.html @@ -0,0 +1,37 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static %} + +{% block extrahead %} + {{ block.super }} + {{ media }} + + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% translate "Submit to the finance department" %}

+

+{% trans "Do you want to submit the statement for further processing by the finance department? If you proceed, no further changes to the statement are possible." %} +

+ +
+ {% csrf_token %} + + + {% translate "Cancel" %} +
+{% endblock %} diff --git a/jdav_web/finance/tests.py b/jdav_web/finance/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/jdav_web/finance/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/jdav_web/finance/views.py b/jdav_web/finance/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/jdav_web/finance/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/jdav_web/jdav_web/settings.py b/jdav_web/jdav_web/settings.py index 7689c3d..1ba12ac 100644 --- a/jdav_web/jdav_web/settings.py +++ b/jdav_web/jdav_web/settings.py @@ -55,9 +55,11 @@ INSTALLED_APPS = [ 'material.apps.MaterialConfig', 'members.apps.MembersConfig', 'mailer.apps.MailerConfig', + 'finance.apps.FinanceConfig', 'ludwigsburgalpin.apps.LudwigsburgalpinConfig', #'easy_select2', 'djcelery_email', + 'nested_admin', 'django_celery_beat', 'jet', 'django.contrib.admin', @@ -199,44 +201,44 @@ JET_SIDE_MENU_COMPACT = True JET_DEFAULT_THEME = 'jdav-green' JET_CHANGE_FORM_SIBLING_LINKS = False -JET_SIDE_MENU_ITEMS = [ - {'app_label': 'auth', 'permissions': ['auth'], 'items': [ - {'name': 'group', 'permissions': ['auth.group'] }, - {'name': 'user', 'permissions': ['auth.user']}, - ]}, - {'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [ - {'name': 'crontabschedule'}, - {'name': 'clockedschedule'}, - {'name': 'intervalschedule'}, - {'name': 'periodictask'}, - {'name': 'solarschedule'}, - ]}, - {'app_label': 'ludwigsburgalpin', 'permissions': ['ludwigsburgalpin'], 'items': [ - {'name': 'termin'}, - ]}, - {'app_label': 'mailer', 'items': [ - {'name': 'message'}, - {'name': 'emailaddress'}, - ]}, - {'app_label': 'members', 'items': [ - {'name': 'member'}, - {'name': 'group'}, - {'name': 'memberlist', 'permissions': ['members.view_memberlist']}, - {'name': 'membernotelist'}, - {'name': 'freizeit'}, - {'name': 'klettertreff'}, - {'name': 'activitycategory', 'permissions': ['members.view_activitycategory']}, - {'name': 'memberunconfirmedproxy', 'permissions': ['members.view_memberunconfirmedproxy']}, - {'name': 'memberwaitinglist', 'permissions': ['members.view_memberwaitinglist']}, - ]}, - {'app_label': 'material', 'items': [ - {'name': 'materialcategory', 'permissions': ['material.view_materialcategory']}, - {'name': 'materialpart'}, - ]}, - {'label': 'Externe Links', 'items' : [ - { 'label': 'Packlisten und Co.', 'url': 'https://cloud.jdav-ludwigsburg.de/index.php/s/qxQCTR8JqYSXXCQ'} - ]}, -] +#JET_SIDE_MENU_ITEMS = [ +# {'app_label': 'auth', 'permissions': ['auth'], 'items': [ +# {'name': 'group', 'permissions': ['auth.group'] }, +# {'name': 'user', 'permissions': ['auth.user']}, +# ]}, +# {'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [ +# {'name': 'crontabschedule'}, +# {'name': 'clockedschedule'}, +# {'name': 'intervalschedule'}, +# {'name': 'periodictask'}, +# {'name': 'solarschedule'}, +# ]}, +# {'app_label': 'ludwigsburgalpin', 'permissions': ['ludwigsburgalpin'], 'items': [ +# {'name': 'termin'}, +# ]}, +# {'app_label': 'mailer', 'items': [ +# {'name': 'message'}, +# {'name': 'emailaddress'}, +# ]}, +# {'app_label': 'members', 'items': [ +# {'name': 'member'}, +# {'name': 'group'}, +# {'name': 'memberlist', 'permissions': ['members.view_memberlist']}, +# {'name': 'membernotelist'}, +# {'name': 'freizeit'}, +# {'name': 'klettertreff'}, +# {'name': 'activitycategory', 'permissions': ['members.view_activitycategory']}, +# {'name': 'memberunconfirmedproxy', 'permissions': ['members.view_memberunconfirmedproxy']}, +# {'name': 'memberwaitinglist', 'permissions': ['members.view_memberwaitinglist']}, +# ]}, +# {'app_label': 'material', 'items': [ +# {'name': 'materialcategory', 'permissions': ['material.view_materialcategory']}, +# {'name': 'materialpart'}, +# ]}, +# {'label': 'Externe Links', 'items' : [ +# { 'label': 'Packlisten und Co.', 'url': 'https://cloud.jdav-ludwigsburg.de/index.php/s/qxQCTR8JqYSXXCQ'} +# ]}, +#] # Waiting list configuration parameters, all numbers are in days diff --git a/jdav_web/jdav_web/urls.py b/jdav_web/jdav_web/urls.py index d17ed73..581d4fc 100644 --- a/jdav_web/jdav_web/urls.py +++ b/jdav_web/jdav_web/urls.py @@ -35,6 +35,7 @@ urlpatterns += i18n_patterns( re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls', namespace="ludwigsburgalpin")), re_path(r'^$', include('startpage.urls', namespace="startpage")), + re_path(r'^_nested_admin/', include('nested_admin.urls')), ) # TODO: django serving from MEDIA_URL should be disabled in production stage diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index f16086e..7868981 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -23,10 +23,13 @@ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\ from django.forms import Textarea, RadioSelect, TypedChoiceField from django.shortcuts import render +import nested_admin + from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff, - MemberWaitingList, + MemberWaitingList, LJPProposal, Intervention, KlettertreffAttendee, ActivityCategory, OldMemberOnList, MemberList, annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy) +from finance.models import Statement, Bill from mailer.mailutils import send as send_mail, get_echo_link, mail_root from django.conf import settings #from easy_select2 import apply_select2 @@ -74,7 +77,7 @@ class RegistrationFilter(admin.SimpleListFilter): # Register your models here. class MemberAdmin(admin.ModelAdmin): fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz', - 'town', 'phone_number', 'phone_number_parents', 'birth_date', 'group', + 'town', 'phone_number', 'phone_number_parents', 'birth_date', 'group', 'iban', 'gets_newsletter', 'registered', 'registration_form', 'active', 'echoed', 'comments'] list_display = ('name', 'birth_date', 'age', 'get_group', 'gets_newsletter', 'registered', 'active', 'echoed', 'comments', 'activity_score') @@ -386,7 +389,7 @@ class FreizeitAdminForm(forms.ModelForm): label=_('Tour type')) tour_approach = TypedChoiceField(choices=Freizeit.tour_approach_choices, coerce=int, - label=_('Tour type')) + label=_('Means of transportation')) class Meta: model = Freizeit @@ -398,13 +401,47 @@ class FreizeitAdminForm(forms.ModelForm): #self.fields['add_member'].queryset = Member.objects.filter(prename__startswith='F') +class BillOnStatementInline(admin.TabularInline): + model = Bill + extra = 0 + sortable_options = [] + fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof'] + formfield_overrides = { + TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} + } + + +class StatementOnListInline(nested_admin.NestedStackedInline): + model = Statement + extra = 1 + sortable_options = [] + fields = ['explanation'] + inlines = [BillOnStatementInline] + + +class InterventionOnLJPInline(admin.TabularInline): + model = Intervention + extra = 0 + sortable_options = [] + formfield_overrides = { + TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 80})} + } + + +class LJPOnListInline(nested_admin.NestedStackedInline): + model = LJPProposal + extra = 1 + sortable_options = [] + inlines = [InterventionOnLJPInline] + + class MemberOnListInline(GenericTabularInline): model = NewMemberOnList extra = 0 formfield_overrides = { - TextField: {'widget': Textarea(attrs={'rows': 1, - 'cols': 40})} + TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } + sortable_options = [] class OldMemberOnListInline(admin.TabularInline): @@ -418,7 +455,7 @@ class MemberNoteListAdmin(admin.ModelAdmin): search_fields = ('name',) ordering = ('-date',) actions = ['generate_summary'] - + def generate_summary(self, request, queryset): """Generates a pdf summary of the given NoteMemberLists """ @@ -428,7 +465,7 @@ class MemberNoteListAdmin(admin.ModelAdmin): filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') # drop umlauts, accents etc. filename = unicodedata.normalize('NFKD', filename).\ - encode('ASCII', 'ignore').decode() + encode('ASCII', 'ignore').decode() filename_tex = filename + '.tex' filename_pdf = filename + '.pdf' @@ -437,17 +474,17 @@ class MemberNoteListAdmin(admin.ModelAdmin): for memberonlist in memberlist.membersonlist.all(): m = memberonlist.member comment = ". ".join(c for c - in (m.comments, - memberonlist.comments) if - c).replace("..", ".") + in (m.comments, + memberonlist.comments) if + c).replace("..", ".") line = '{0} {1} & {2} \\\\'.format( - esc_ampersand(m.prename), esc_ampersand(m.lastname), - esc_ampersand(comment) or "---") + esc_ampersand(m.prename), esc_ampersand(m.lastname), + esc_ampersand(comment) or "---") table += esc_underscore(line) # copy template shutil.copy(media_path('memberlistnote_template.tex'), - media_path(filename_tex)) + media_path(filename_tex)) # read in template with open(media_path(filename_tex), 'r', encoding='utf-8') as f: @@ -543,8 +580,8 @@ class MemberListAdmin(admin.ModelAdmin): messages.info(request, "Teilnehmerlist(en) erfolgreich erstellt.") migrate_to_notelist.short_description = "Aus Teilnehmerliste(n) Notizliste erstellen" -class FreizeitAdmin(admin.ModelAdmin): - inlines = [MemberOnListInline] +class FreizeitAdmin(nested_admin.NestedModelAdmin): + inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline] form = FreizeitAdminForm list_display = ['__str__', 'date'] search_fields = ('name',) diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 54b633f..effdab8 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -14,6 +14,7 @@ from mailer.mailutils import send as send_mail, mail_root, get_mail_confirmation prepend_base_url, get_registration_link, get_wait_confirmation_link from django.contrib.auth.models import User from django.conf import settings +from django.core.validators import MinValueValidator from dateutil.relativedelta import relativedelta @@ -160,6 +161,8 @@ class Member(Person): phone_number_parents = models.CharField(max_length=18, verbose_name=_('parents phone number'), default='', blank=True) group = models.ManyToManyField(Group, verbose_name=_('group')) + iban = models.CharField(max_length=30, blank=True, verbose_name='IBAN') + gets_newsletter = models.BooleanField(_('receives newsletter'), default=True) unsubscribe_key = models.CharField(max_length=32, default="") @@ -517,10 +520,13 @@ class Freizeit(models.Model): # verbose_name is overriden by form, label is set in admin.py tour_type = models.IntegerField(choices=tour_type_choices) tour_approach_choices = ((MUSKELKRAFT_ANREISE, 'Muskelkraft'), - (OEFFENTLICHE_ANREISE, 'Öffentliche VM'), - (FAHRGEMEINSCHAFT_ANREISE, 'Fahrgemeinschaften')) + (OEFFENTLICHE_ANREISE, 'ÖPNV'), + (FAHRGEMEINSCHAFT_ANREISE, 'Fahrgemeinschaften')) tour_approach = models.IntegerField(choices=tour_approach_choices, - default=MUSKELKRAFT_ANREISE) + default=MUSKELKRAFT_ANREISE, + verbose_name=_('Means of transportation')) + kilometers_traveled = models.IntegerField(verbose_name=_('Kilometers traveled'), + validators=[MinValueValidator(0)]) activity = models.ManyToManyField(ActivityCategory, default=None, verbose_name=_('Categories')) difficulty_choices = [(1, _('easy')), (2, _('medium')), (3, _('hard'))] @@ -634,6 +640,37 @@ class RegistrationPassword(models.Model): verbose_name_plural = _('registration passwords') +class LJPProposal(models.Model): + """A proposal for LJP""" + title = models.CharField(verbose_name=_('Title'), max_length=30) + + goals_alpinistic = models.TextField(verbose_name=_('Alpinistic goals')) + goals_pedagogic = models.TextField(verbose_name=_('Pedagogic goals')) + methods = models.TextField(verbose_name=_('Content and methods')) + evaluation = models.TextField(verbose_name=_('Evaluation')) + experiences = models.TextField(verbose_name=_('Experiences and possible improvements')) + + excursion = models.OneToOneField(Freizeit, + verbose_name=_('Excursion'), + blank=True, + null=True, + on_delete=models.SET_NULL) + + +class Intervention(models.Model): + """An intervention during a seminar as part of a LJP proposal""" + date_start = models.DateTimeField(verbose_name=_('Starting time')) + duration = models.DecimalField(verbose_name=_('Duration in hours'), + max_digits=3, + decimal_places=2) + activity = models.TextField(verbose_name=_('Activity and method')) + + ljp_proposal = models.ForeignKey(LJPProposal, + verbose_name=_('LJP Proposal'), + blank=False, + on_delete=models.CASCADE) + + def annotate_activity_score(queryset): one_year_ago = datetime.now() - timedelta(days=365) queryset = queryset.annotate( diff --git a/jdav_web/templates/admin/finance/statement/change_form_object_tools.html b/jdav_web/templates/admin/finance/statement/change_form_object_tools.html new file mode 100644 index 0000000..54fc985 --- /dev/null +++ b/jdav_web/templates/admin/finance/statement/change_form_object_tools.html @@ -0,0 +1,15 @@ +{% extends "admin/change_form_object_tools.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} + +{% if not original.submitted %} +
  • + {% url opts|admin_urlname:'submit' original.pk|admin_urlquote as invite_url %} + {% trans 'Submit' %} +
  • +{% endif %} + +{{block.super}} + +{% endblock %} diff --git a/jdav_web/templates/admin/finance/statementsubmitted/change_form_object_tools.html b/jdav_web/templates/admin/finance/statementsubmitted/change_form_object_tools.html new file mode 100644 index 0000000..0a51b90 --- /dev/null +++ b/jdav_web/templates/admin/finance/statementsubmitted/change_form_object_tools.html @@ -0,0 +1,13 @@ +{% extends "admin/change_form_object_tools.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} + +
  • + {% url opts|admin_urlname:'overview' original.pk|admin_urlquote as invite_url %} + {% trans 'Overview' %} +
  • + +{{block.super}} + +{% endblock %}