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',
'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

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

@ -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):
@ -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',)

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

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