finance: add initial structre, including models and admin page, add some customization

v1-0-stable
Christian Merten 3 years ago
parent de2c5081e2
commit cafc7f4f97
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -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(
"<path:object_id>/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(
"<path:object_id>/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']

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FinanceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'finance'

@ -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()

@ -0,0 +1,55 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' statement.pk|admin_urlquote %}">{{ statement|truncatewords:"18" }}</a>
&rsaquo; {% translate 'Submit' %}
</div>
{% endblock %}
{% block content %}
<h2>{% translate "Overview" %}</h2>
<ul>
{% for bill in statement.bill_set.all %}
<li>
{{bill.short_description}}
</li>
{% endfor %}
</ul>
<p>{% blocktrans %}The total amount is {{total_bills}} €.{% endblocktrans %}</p>
{% if statement.excursion %}
<h3>{% trans "Excursion" %}</h3>
<p>{% blocktrans %}Total distance traveled: {{ statement.excursion.kilometers_traveled }} km by
{{ statement.excursion.tour_approach }}. This results in {{ total_transportation }} €.{% endblocktrans %}
</p>
{% endif %}
{% blocktrans %} This results in a total amount of {{ total }} € {% endblocktrans %}
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="submit_statement">
<input class="default" style="color: $default-link-color" type="submit" name="apply" value="{% translate 'Submit' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% endblock %}

@ -0,0 +1,37 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' statement.pk|admin_urlquote %}">{{ statement|truncatewords:"18" }}</a>
&rsaquo; {% translate 'Submit' %}
</div>
{% endblock %}
{% block content %}
<h2>{% translate "Submit to the finance department" %}</h2>
<p>
{% 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." %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="submit_statement">
<input class="default" style="color: $default-link-color" type="submit" name="apply" value="{% translate 'Submit' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% endblock %}

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -55,9 +55,11 @@ INSTALLED_APPS = [
'material.apps.MaterialConfig', 'material.apps.MaterialConfig',
'members.apps.MembersConfig', 'members.apps.MembersConfig',
'mailer.apps.MailerConfig', 'mailer.apps.MailerConfig',
'finance.apps.FinanceConfig',
'ludwigsburgalpin.apps.LudwigsburgalpinConfig', 'ludwigsburgalpin.apps.LudwigsburgalpinConfig',
#'easy_select2', #'easy_select2',
'djcelery_email', 'djcelery_email',
'nested_admin',
'django_celery_beat', 'django_celery_beat',
'jet', 'jet',
'django.contrib.admin', 'django.contrib.admin',
@ -199,44 +201,44 @@ JET_SIDE_MENU_COMPACT = True
JET_DEFAULT_THEME = 'jdav-green' JET_DEFAULT_THEME = 'jdav-green'
JET_CHANGE_FORM_SIBLING_LINKS = False JET_CHANGE_FORM_SIBLING_LINKS = False
JET_SIDE_MENU_ITEMS = [ #JET_SIDE_MENU_ITEMS = [
{'app_label': 'auth', 'permissions': ['auth'], 'items': [ # {'app_label': 'auth', 'permissions': ['auth'], 'items': [
{'name': 'group', 'permissions': ['auth.group'] }, # {'name': 'group', 'permissions': ['auth.group'] },
{'name': 'user', 'permissions': ['auth.user']}, # {'name': 'user', 'permissions': ['auth.user']},
]}, # ]},
{'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [ # {'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [
{'name': 'crontabschedule'}, # {'name': 'crontabschedule'},
{'name': 'clockedschedule'}, # {'name': 'clockedschedule'},
{'name': 'intervalschedule'}, # {'name': 'intervalschedule'},
{'name': 'periodictask'}, # {'name': 'periodictask'},
{'name': 'solarschedule'}, # {'name': 'solarschedule'},
]}, # ]},
{'app_label': 'ludwigsburgalpin', 'permissions': ['ludwigsburgalpin'], 'items': [ # {'app_label': 'ludwigsburgalpin', 'permissions': ['ludwigsburgalpin'], 'items': [
{'name': 'termin'}, # {'name': 'termin'},
]}, # ]},
{'app_label': 'mailer', 'items': [ # {'app_label': 'mailer', 'items': [
{'name': 'message'}, # {'name': 'message'},
{'name': 'emailaddress'}, # {'name': 'emailaddress'},
]}, # ]},
{'app_label': 'members', 'items': [ # {'app_label': 'members', 'items': [
{'name': 'member'}, # {'name': 'member'},
{'name': 'group'}, # {'name': 'group'},
{'name': 'memberlist', 'permissions': ['members.view_memberlist']}, # {'name': 'memberlist', 'permissions': ['members.view_memberlist']},
{'name': 'membernotelist'}, # {'name': 'membernotelist'},
{'name': 'freizeit'}, # {'name': 'freizeit'},
{'name': 'klettertreff'}, # {'name': 'klettertreff'},
{'name': 'activitycategory', 'permissions': ['members.view_activitycategory']}, # {'name': 'activitycategory', 'permissions': ['members.view_activitycategory']},
{'name': 'memberunconfirmedproxy', 'permissions': ['members.view_memberunconfirmedproxy']}, # {'name': 'memberunconfirmedproxy', 'permissions': ['members.view_memberunconfirmedproxy']},
{'name': 'memberwaitinglist', 'permissions': ['members.view_memberwaitinglist']}, # {'name': 'memberwaitinglist', 'permissions': ['members.view_memberwaitinglist']},
]}, # ]},
{'app_label': 'material', 'items': [ # {'app_label': 'material', 'items': [
{'name': 'materialcategory', 'permissions': ['material.view_materialcategory']}, # {'name': 'materialcategory', 'permissions': ['material.view_materialcategory']},
{'name': 'materialpart'}, # {'name': 'materialpart'},
]}, # ]},
{'label': 'Externe Links', 'items' : [ # {'label': 'Externe Links', 'items' : [
{ 'label': 'Packlisten und Co.', 'url': 'https://cloud.jdav-ludwigsburg.de/index.php/s/qxQCTR8JqYSXXCQ'} # { 'label': 'Packlisten und Co.', 'url': 'https://cloud.jdav-ludwigsburg.de/index.php/s/qxQCTR8JqYSXXCQ'}
]}, # ]},
] #]
# Waiting list configuration parameters, all numbers are in days # Waiting list configuration parameters, all numbers are in days

@ -35,6 +35,7 @@ urlpatterns += i18n_patterns(
re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls', re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls',
namespace="ludwigsburgalpin")), namespace="ludwigsburgalpin")),
re_path(r'^$', include('startpage.urls', namespace="startpage")), 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 # TODO: django serving from MEDIA_URL should be disabled in production stage

@ -23,10 +23,13 @@ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\
from django.forms import Textarea, RadioSelect, TypedChoiceField from django.forms import Textarea, RadioSelect, TypedChoiceField
from django.shortcuts import render from django.shortcuts import render
import nested_admin
from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff, from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff,
MemberWaitingList, MemberWaitingList, LJPProposal, Intervention,
KlettertreffAttendee, ActivityCategory, OldMemberOnList, MemberList, KlettertreffAttendee, ActivityCategory, OldMemberOnList, MemberList,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy) 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 mailer.mailutils import send as send_mail, get_echo_link, mail_root
from django.conf import settings from django.conf import settings
#from easy_select2 import apply_select2 #from easy_select2 import apply_select2
@ -74,7 +77,7 @@ class RegistrationFilter(admin.SimpleListFilter):
# Register your models here. # Register your models here.
class MemberAdmin(admin.ModelAdmin): class MemberAdmin(admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz', 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'] 'gets_newsletter', 'registered', 'registration_form', 'active', 'echoed', 'comments']
list_display = ('name', 'birth_date', 'age', 'get_group', 'gets_newsletter', list_display = ('name', 'birth_date', 'age', 'get_group', 'gets_newsletter',
'registered', 'active', 'echoed', 'comments', 'activity_score') 'registered', 'active', 'echoed', 'comments', 'activity_score')
@ -386,7 +389,7 @@ class FreizeitAdminForm(forms.ModelForm):
label=_('Tour type')) label=_('Tour type'))
tour_approach = TypedChoiceField(choices=Freizeit.tour_approach_choices, tour_approach = TypedChoiceField(choices=Freizeit.tour_approach_choices,
coerce=int, coerce=int,
label=_('Tour type')) label=_('Means of transportation'))
class Meta: class Meta:
model = Freizeit model = Freizeit
@ -398,13 +401,47 @@ class FreizeitAdminForm(forms.ModelForm):
#self.fields['add_member'].queryset = Member.objects.filter(prename__startswith='F') #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): class MemberOnListInline(GenericTabularInline):
model = NewMemberOnList model = NewMemberOnList
extra = 0 extra = 0
formfield_overrides = { formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
'cols': 40})}
} }
sortable_options = []
class OldMemberOnListInline(admin.TabularInline): class OldMemberOnListInline(admin.TabularInline):
@ -543,8 +580,8 @@ class MemberListAdmin(admin.ModelAdmin):
messages.info(request, "Teilnehmerlist(en) erfolgreich erstellt.") messages.info(request, "Teilnehmerlist(en) erfolgreich erstellt.")
migrate_to_notelist.short_description = "Aus Teilnehmerliste(n) Notizliste erstellen" migrate_to_notelist.short_description = "Aus Teilnehmerliste(n) Notizliste erstellen"
class FreizeitAdmin(admin.ModelAdmin): class FreizeitAdmin(nested_admin.NestedModelAdmin):
inlines = [MemberOnListInline] inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline]
form = FreizeitAdminForm form = FreizeitAdminForm
list_display = ['__str__', 'date'] list_display = ['__str__', 'date']
search_fields = ('name',) search_fields = ('name',)

@ -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 prepend_base_url, get_registration_link, get_wait_confirmation_link
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator
from dateutil.relativedelta import relativedelta 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) phone_number_parents = models.CharField(max_length=18, verbose_name=_('parents phone number'), default='', blank=True)
group = models.ManyToManyField(Group, verbose_name=_('group')) group = models.ManyToManyField(Group, verbose_name=_('group'))
iban = models.CharField(max_length=30, blank=True, verbose_name='IBAN')
gets_newsletter = models.BooleanField(_('receives newsletter'), gets_newsletter = models.BooleanField(_('receives newsletter'),
default=True) default=True)
unsubscribe_key = models.CharField(max_length=32, default="") 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 # verbose_name is overriden by form, label is set in admin.py
tour_type = models.IntegerField(choices=tour_type_choices) tour_type = models.IntegerField(choices=tour_type_choices)
tour_approach_choices = ((MUSKELKRAFT_ANREISE, 'Muskelkraft'), tour_approach_choices = ((MUSKELKRAFT_ANREISE, 'Muskelkraft'),
(OEFFENTLICHE_ANREISE, 'Öffentliche VM'), (OEFFENTLICHE_ANREISE, 'ÖPNV'),
(FAHRGEMEINSCHAFT_ANREISE, 'Fahrgemeinschaften')) (FAHRGEMEINSCHAFT_ANREISE, 'Fahrgemeinschaften'))
tour_approach = models.IntegerField(choices=tour_approach_choices, 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, activity = models.ManyToManyField(ActivityCategory, default=None,
verbose_name=_('Categories')) verbose_name=_('Categories'))
difficulty_choices = [(1, _('easy')), (2, _('medium')), (3, _('hard'))] difficulty_choices = [(1, _('easy')), (2, _('medium')), (3, _('hard'))]
@ -634,6 +640,37 @@ class RegistrationPassword(models.Model):
verbose_name_plural = _('registration passwords') 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): def annotate_activity_score(queryset):
one_year_ago = datetime.now() - timedelta(days=365) one_year_ago = datetime.now() - timedelta(days=365)
queryset = queryset.annotate( queryset = queryset.annotate(

@ -0,0 +1,15 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls %}
{% block object-tools-items %}
{% if 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 %}

@ -0,0 +1,13 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls %}
{% block object-tools-items %}
<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>
{{block.super}}
{% endblock %}
Loading…
Cancel
Save