finance: add initial structre, including models and admin page, add some customization
parent
de2c5081e2
commit
cafc7f4f97
@ -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>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' statement.pk|admin_urlquote %}">{{ statement|truncatewords:"18" }}</a>
|
||||
› {% 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>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' statement.pk|admin_urlquote %}">{{ statement|truncatewords:"18" }}</a>
|
||||
› {% 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.
|
||||
@ -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…
Reference in New Issue