finance: improve confirm overview, polish admin pages

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

@ -7,11 +7,13 @@ 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, StatementConfirmed
from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted, StatementConfirmed,\
StatementUnSubmitted
@admin.register(Ledger)
class LedgerAdmin(admin.ModelAdmin):
pass
search_fields = ('name', )
class BillOnStatementInline(admin.TabularInline):
@ -29,8 +31,8 @@ class BillOnStatementInline(admin.TabularInline):
return super(BillOnStatementInline, self).get_readonly_fields(request, obj)
@admin.register(Statement)
class StatementAdmin(admin.ModelAdmin):
@admin.register(StatementUnSubmitted)
class StatementUnSubmitteddAdmin(admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'submitted']
inlines = [BillOnStatementInline]
@ -65,13 +67,13 @@ class StatementAdmin(admin.ModelAdmin):
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,)))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
if "apply" in request.POST:
statement.submit()
statement.submit(get_member(request))
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,)))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
context = dict(self.admin_site.each_context(request),
title=_('Submit statement'),
opts=self.opts,
@ -82,7 +84,7 @@ class StatementAdmin(admin.ModelAdmin):
class TransactionOnSubmittedStatementInline(admin.TabularInline):
model = Transaction
fields = ['amount', 'member', 'reference']
fields = ['amount', 'member', 'reference', 'ledger']
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
@ -105,6 +107,8 @@ class BillOnSubmittedStatementInline(BillOnStatementInline):
@admin.register(StatementSubmitted)
class StatementSubmittedAdmin(admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'submitted']
list_display = ['__str__', 'is_valid', 'submitted_date', 'submitted_by']
ordering = ('-submitted_date',)
inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def has_add_permission(self, request, obj=None):
@ -142,17 +146,50 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
return custom_urls + urls
def overview_view(self, request, object_id):
statement = Statement.objects.get(pk=object_id)
statement = StatementSubmitted.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 "transaction_execution_confirm" in request.POST:
res = statement.confirm(confirmer=get_member(request))
if not res:
# this should NOT happen!
messages.error(request,
_("An error occured while trying to confirm %(name)s. Please try again.") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name)))
messages.success(request,
_("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.")
% {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "confirm" in request.POST:
statement.confirmed = True
res = statement.validity
if res == Statement.VALID:
context = dict(self.admin_site.each_context(request),
title=_('Statement confirmed'),
opts=self.opts,
statement=statement)
return render(request, 'admin/confirmed_statement.html', context=context)
elif res == Statement.NON_MATCHING_TRANSACTIONS:
messages.error(request,
_("Transactions do not match the covered expenses. Please correct the mistakes listed below.")
% {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
elif res == Statement.MISSING_LEDGER:
messages.error(request,
_("Some transactions have no ledger configured. Please fill in the gaps.")
% {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "reject" in request.POST:
statement.submitted = False
statement.save()
for trans in statement.transaction_set.all():
trans.confirmed = True
trans.save()
messages.success(request,
_("Successfully rejected %(name)s. The requestor can reapply, when needed.")
% {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "generate_transactions" in request.POST:
if statement.transaction_set.count() > 0:
@ -167,6 +204,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
title=_('View submitted statement'),
opts=self.opts,
statement=statement,
transaction_issues=statement.transaction_issues,
nights=statement.excursion.night_count,
price_per_night=statement.real_night_cost,
duration=statement.excursion.duration,
@ -181,33 +219,49 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
transportation_per_yl=statement.transportation_per_yl,
total_per_yl=statement.total_per_yl,
total_staff=statement.total_staff,
total=statement.total())
total=statement.total)
return render(request, 'admin/overview_submitted_statement.html', context=context)
def reduce_transactions_view(self, request, object_id):
statement = Statement.objects.get(pk=object_id)
statement = StatementSubmitted.objects.get(pk=object_id)
statement.reduce_transactions()
messages.success(request,
_("Successfully reduced transactions for %(name)s.") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
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', 'confirmed']
readonly_fields = fields
#readonly_fields = fields
list_display = ['__str__', 'total_pretty', 'confirmed_date', 'confirmed_by']
ordering = ('-confirmed_date',)
@admin.register(Receipt)
class ReceiptAdmin(admin.ModelAdmin):
pass
def has_add_permission(self, request, obj=None):
return False
@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
pass
list_display = ['member', 'ledger', 'amount', 'reference', 'statement', 'confirmed',
'confirmed_date', 'confirmed_by']
list_filter = ('ledger', 'member', 'statement', 'confirmed')
search_fields = ('reference', )
fields = ['reference', 'amount', 'member', 'ledger', 'statement']
readonly_fields = fields
@admin.register(Bill)
class BillAdmin(admin.ModelAdmin):
list_display = ['short_description', 'pretty_amount', 'paid_by', 'refunded']
list_display = ['__str__', 'statement', 'short_description', 'pretty_amount', 'paid_by', 'refunded']
list_filter = ('statement', 'paid_by', 'refunded')
search_fields = ('reference', 'statement')
def get_member(request):
if not hasattr(request.user, 'member'):
return None
else:
return request.user.member

@ -1,5 +1,7 @@
import math
from itertools import groupby
from decimal import Decimal, ROUND_HALF_DOWN
from django.utils import timezone
from django.db import models
from django.utils.translation import gettext_lazy as _
@ -13,8 +15,28 @@ class Ledger(models.Model):
def __str__(self):
return self.name
class Meta:
verbose_name = _('Ledger')
verbose_name_plural = _('Ledgers')
class TransactionIssue:
def __init__(self, member, current, target):
self.member, self.current, self. target = member, current, target
@property
def difference(self):
return self.target - self.current
class StatementManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(submitted=False, confirmed=False)
class Statement(models.Model):
MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, VALID = 0, 1, 2
ALLOWANCE_PER_DAY = 10
short_description = models.CharField(verbose_name=_('Short description'),
@ -30,9 +52,24 @@ class Statement(models.Model):
night_cost = models.DecimalField(verbose_name=_('Price per night'), default=0, decimal_places=2, max_digits=3)
submitted = models.BooleanField(verbose_name=_('Submitted'), default=False)
submitted_date = models.DateTimeField(verbose_name=_('Submitted on'), default=None, null=True)
confirmed = models.BooleanField(verbose_name=_('Confirmed'), default=False)
confirmed_date = models.DateTimeField(verbose_name=_('Paid on'), default=None, null=True)
submitted_by = models.ForeignKey(Member, verbose_name=_('Submitted by'),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='submitted_statements')
confirmed_by = models.ForeignKey(Member, verbose_name=_('Authorized by'),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='confirmed_statements')
class Meta:
verbose_name = _('Statement')
verbose_name_plural = _('Statements')
permissions = [('may_edit_submitted_statements', 'Is allowed to edit submitted statements')]
def __str__(self):
@ -41,10 +78,74 @@ class Statement(models.Model):
else:
return self.short_description
def submit(self):
def submit(self, submitter=None):
self.submitted = True
self.submitted_date = timezone.now()
self.submitted_by = submitter
self.save()
@property
def transaction_issues(self):
needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered]
if self.excursion is not None:
needed_paiments.extend([(yl, self.real_per_yl) for yl in self.excursion.jugendleiter.all()])
needed_paiments = sorted(needed_paiments, key=lambda p: p[0].pk)
target = map(lambda p: (p[0], sum([x[1] for x in p[1]])), groupby(needed_paiments, lambda p: p[0]))
transactions = sorted(self.transaction_set.all(), key=lambda trans: trans.member.pk)
current = dict(map(lambda p: (p[0], sum([t.amount for t in p[1]])), groupby(transactions, lambda trans: trans.member)))
issues = []
for member, amount in target:
if amount == 0:
continue
elif member not in current:
issue = TransactionIssue(member=member, current=0, target=amount)
issues.append(issue)
elif current[member] != amount:
issue = TransactionIssue(member=member, current=current[member], target=amount)
issues.append(issue)
return issues
@property
def ledgers_configured(self):
return all([trans.ledger is not None for trans in self.transaction_set.all()])
@property
def transactions_match_expenses(self):
return len(self.transaction_issues) == 0
def is_valid(self):
return self.ledgers_configured and self.transactions_match_expenses
is_valid.boolean = True
is_valid.short_description = _('Ready to confirm')
@property
def validity(self):
if not self.transactions_match_expenses:
return Statement.NON_MATCHING_TRANSACTIONS
if not self.ledgers_configured:
return Statement.MISSING_LEDGER
else:
return Statement.VALID
def confirm(self, confirmer=None):
if not self.validity == Statement.VALID:
return False
self.confirmed = True
self.confirmed_date = timezone.now()
self.confirmed_by = confirmer
for trans in self.transaction_set.all():
trans.confirmed = True
trans.confirmed_date = timezone.now()
trans.confirmed_by = confirmer
trans.save()
self.save()
return True
def generate_transactions(self):
# bills
for bill in self.bill_set.all():
@ -58,20 +159,29 @@ class Statement(models.Model):
return
for yl in self.excursion.jugendleiter.all():
real_per_yl = self.total_staff / self.excursion.jugendleiter.count()
ref = _("Compensation for %(excu)s.") % {'excu': self.excursion.name}
Transaction(statement=self, member=yl, amount=real_per_yl, confirmed=False, reference=ref).save()
ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=yl, amount=self.real_per_yl, confirmed=False, reference=ref).save()
def reduce_transactions(self):
transactions = sorted(self.transaction_set.all(), key=lambda trans: trans.member.pk)
for member, transaction_group in groupby(transactions, lambda trans: trans.member):
# to minimize the number of needed bank transactions, we bundle transactions from same ledger to
# same member
transactions = self.transaction_set.all()
if any((t.ledger is None for t in transactions)):
return
sort_key = lambda trans: (trans.member.pk, trans.ledger.pk)
group_key = lambda trans: (trans.member, trans.ledger)
transactions = sorted(transactions, key=sort_key)
for pair, transaction_group in groupby(transactions, group_key):
member, ledger = pair
grp = list(transaction_group)
if len(grp) == 1:
continue
new_amount = sum((trans.amount for trans in grp))
new_ref = "\n".join((trans.reference for trans in grp))
Transaction(statement=self, member=member, amount=new_amount, confirmed=False, reference=new_ref).save()
Transaction(statement=self, member=member, amount=new_amount, confirmed=False, reference=new_ref,
ledger=ledger).save()
for trans in grp:
trans.delete()
@ -95,14 +205,14 @@ class Statement(models.Model):
if self.excursion is None:
return 0
return self.excursion.kilometers_traveled * self.euro_per_km
return cvt_to_decimal(self.excursion.kilometers_traveled * self.euro_per_km)
@property
def allowance_per_yl(self):
if self.excursion is None:
return 0
return self.excursion.duration * self.ALLOWANCE_PER_DAY
return cvt_to_decimal(self.excursion.duration * self.ALLOWANCE_PER_DAY)
@property
def real_night_cost(self):
@ -113,7 +223,7 @@ class Statement(models.Model):
if self.excursion is None:
return 0
return float(self.excursion.night_count * self.real_night_cost)
return self.excursion.night_count * self.real_night_cost
@property
def total_per_yl(self):
@ -121,6 +231,13 @@ class Statement(models.Model):
+ self.allowance_per_yl \
+ self.nights_per_yl
@property
def real_per_yl(self):
if self.excursion is None:
return 0
return self.total_staff / self.excursion.staff_count
@property
def total_staff(self):
return self.total_per_yl * self.real_staff_count
@ -148,8 +265,27 @@ class Statement(models.Model):
else:
return 2 + math.ceil((participant_count - 7) / 7)
@property
def total(self):
return float(self.total_bills) + self.total_staff
return self.total_bills + self.total_staff
def total_pretty(self):
return "{}".format(self.total)
total_pretty.short_description = _('Total')
class StatementUnSubmittedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(submitted=False, confirmed=False)
class StatementUnSubmitted(Statement):
objects = StatementUnSubmittedManager()
class Meta:
proxy = True
verbose_name = _('Statement in preparation')
verbose_name_plural = _('Statements in preparation')
class StatementSubmittedManager(models.Manager):
@ -177,8 +313,8 @@ class StatementConfirmed(Statement):
class Meta:
proxy = True
verbose_name = _('Confirmed statement')
verbose_name_plural = _('Confirmed statements')
verbose_name = _('Paid statement')
verbose_name_plural = _('Paid statements')
permissions = (('may_manage_confirmed_statements', 'Can view and manage confirmed statements.'),)
@ -188,7 +324,7 @@ class Bill(models.Model):
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,
paid_by = models.ForeignKey(Member, verbose_name=_('Authorized by'), null=True,
on_delete=models.SET_NULL)
costs_covered = models.BooleanField(verbose_name=_('Covered'), default=False)
refunded = models.BooleanField(verbose_name=_('Refunded'), default=False)
@ -199,20 +335,39 @@ class Bill(models.Model):
return "{} ({}€)".format(self.short_description, self.amount)
def pretty_amount(self):
return "{} ".format(self.amount)
return "{}".format(self.amount)
pretty_amount.admin_order_field = 'amount'
class Meta:
verbose_name = _('Bill')
verbose_name_plural = _('Bills')
class Transaction(models.Model):
reference = models.TextField(verbose_name=_('Reference'))
amount = models.DecimalField(max_digits=6, decimal_places=2)
member = models.ForeignKey(Member, verbose_name=_('Recipient'),
on_delete=models.CASCADE)
ledger = models.ForeignKey(Ledger, blank=False, null=True, default=None, verbose_name=_('Ledger'),
on_delete=models.SET_NULL)
statement = models.ForeignKey(Statement, verbose_name=_('Statement'),
on_delete=models.CASCADE)
confirmed = models.BooleanField(verbose_name=_('Confirmed'), default=False)
confirmed = models.BooleanField(verbose_name=_('Paid'), default=False)
confirmed_date = models.DateTimeField(verbose_name=_('Paid on'), default=None, null=True)
confirmed_by = models.ForeignKey(Member, verbose_name=_('Authorized by'),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='confirmed_transactions')
def __str__(self):
return "T#{}".format(self.pk)
class Meta:
verbose_name = _('Transaction')
verbose_name_plural = _('Transactions')
class Receipt(models.Model):
@ -221,3 +376,7 @@ class Receipt(models.Model):
on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=6, decimal_places=2)
comments = models.TextField()
def cvt_to_decimal(f):
return Decimal(f).quantize(Decimal('.01'), rounding=ROUND_HALF_DOWN)

@ -0,0 +1,72 @@
{% 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 }} admin-view
{% 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 'Paiment' %}
</div>
{% endblock %}
{% block content %}
<h2>{% translate "Paying statement" %}</h2>
<p>
{% blocktrans %}The statement is valid. Please execute the following transactions and then proceed by
finalizing the confirmation.
{% endblocktrans %}
</p>
<p>
<table>
<th>
<td>{% trans "IBAN" %}</td>
<td>{% trans "Amount" %}</td>
<td>{% trans "Reference" %}</td>
<td>{% trans "Ledger" %}</td>
</th>
{% for transaction in statement.transaction_set.all %}
<tr>
<td>
{{ transaction.member }}
</td>
<td>
{{ transaction.member.iban }}
</td>
<td>
{{ transaction.amount }}€
</td>
<td>
{{ transaction.reference }}
</td>
<td>
{{ transaction.ledger }}
</td>
</tr>
{% endfor %}
</table>
</p>
<form action="" method="post">
{% csrf_token %}
<p>
<input type="checkbox" required>
{% blocktrans %}I did execute the listed transactions.{% endblocktrans %}
</p>
<input class="default confirm" type="submit" name="transaction_execution_confirm" value="{% translate 'Confirm' %}">
</form>
{% endblock %}

@ -1,8 +1,6 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% load finance_extras %}
{% block extrahead %}
{{ block.super }}
{{ media }}
@ -11,7 +9,7 @@
<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
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} admin-view
{% endblock %}
{% block breadcrumbs %}
@ -28,16 +26,25 @@
<h2>{% translate "Bills" %}</h2>
<p>
<ul>
<table>
<th>
<td>Amount</td>
<td>Covered by association</td>
</th>
{% for bill in statement.bill_set.all %}
<li>
{{bill.short_description}} {%trans "for" %} {{ bill.amount }}€.
{% if not bill.costs_covered %}
This bill is not covered by the association.
{% endif %}
</li>
<tr>
<td>
{{bill.short_description}}
</td>
<td>
{{ bill.amount }}€.
</td>
<td>
{{ bill.costs_covered }}
</td>
</tr>
{% endfor %}
</ul>
</table>
</p>
<p>{% blocktrans %}The total amount is {{total_bills}} €.{% endblocktrans %}</p>
@ -76,12 +83,101 @@ In total this is {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_
{% blocktrans %} This results in a total amount of {{ total }} € {% endblocktrans %}
</p>
<h2>{% trans 'Transactions' %}</h2>
{% if statement.transaction_set.count == 0 %}
{% blocktrans %}Currently, no transactions are planned. You can auto generate them from the data, by clicking the button at the
end of the page.{% endblocktrans %}
<form method="post">
{% csrf_token %}
<input class="default other" type="submit" name="generate_transactions" value="{% translate 'Generate transactions' %}">
</form>
{% else %}
{% blocktrans %}Currently the following transactions are planned.{% endblocktrans %}
<p>
<table>
<th>
<td>Amount</td>
<td>Reference</td>
<td>Ledger</td>
</th>
{% for transaction in statement.transaction_set.all %}
<tr>
<td>
{{ transaction.member }}
</td>
<td>
{{ transaction.amount }}€
</td>
<td>
{{ transaction.reference }}
</td>
<td>
{{ transaction.ledger }}
</td>
</tr>
{% endfor %}
</table>
{% comment %}
<ul>
{% for transaction in statement.transaction_set.all %}
<li>
{{ transaction.member }}
{% trans "receives" %}
{{ transaction.amount }}€
{% trans "for" %}
{{ transaction.reference }},
{% trans "paid with" %}
{{ transaction.ledger }}.
</li>
{% endfor %}
</ul>
{% endcomment%}
</p>
{% if transaction_issues|length == 0 %}
{% blocktrans %}These transactions match the calculated costs.{% endblocktrans %}
{% else %}
<p>
{% blocktrans %}The current transactions do not reflect all costs in this statement. Please fix the following issues:{% endblocktrans %}
</p>
<p>
<table>
<th>
<td>Currently receiving</td>
<td>Actual costs</td>
<td>Difference</td>
</th>
{% for issue in transaction_issues %}
<tr>
<td>
{{ issue.member }}
</td>
<td>
{{ issue.current }}€
</td>
<td>
{{ issue.target }}€
</td>
<td>
{{ issue.difference }}€
</td>
</tr>
{% endfor %}
</table>
</p>
{% endif %}
{% endif %}
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="submit_statement">
<input class="default" type="submit" name="confirm" value="{% translate 'Accept' %}">
<input class="default" type="submit" name="reject" value="{% translate 'Reject' %}">
<input class="default" type="submit" name="generate_transactions" value="{% translate 'Generate transactions' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
<input class="default confirm" type="submit" name="confirm" value="{% translate 'Accept' %}">
<input class="default danger" type="submit" name="reject" value="{% translate 'Reject' %}">
{% url opts|admin_urlname:'change' original.pk|admin_urlquote as change_url %}
<a class="button cancel-link" href="{% add_preserved_filters change_url %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

@ -0,0 +1,193 @@
@import "globals";
.admin-view {
#content > ul, #content > h1 + p + ul {
background: $content-background-color;
border-radius: 4px;
box-shadow: 0 2px 0 0 $content-border2-color;
&, ul {
list-style-type: none;
margin: 0;
padding: 0;
li {
list-style: disc;
line-height: 1.8;
}
}
ul {
margin-left: 20px;
}
> li {
padding: 8px;
border-bottom: 1px solid $content-border-color;
font-size: 13px;
list-style: none;
&:last-child {
border-bottom: 0;
}
}
}
#content form {
margin-top: 20px;
input[type="submit"].danger {
background-color: $danger-button-background-color;
color: $danger-button-text-color;
font-size: 12px;
font-weight: lighter;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px;
@include for-mobile {
display: block;
width: 100%;
}
&:hover, &:focus {
background-color: $button-hover-background-color;
color: $button-hover-text-color;
}
&:active {
background-color: $button-active-background-color;
color: $button-active-text-color;
}
}
input[type="submit"].confirm {
background-color: $primary-button-background-color;
color: $primary-button-text-color;
font-size: 12px;
font-weight: lighter;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px;
@include for-mobile {
display: block;
width: 100%;
}
&:hover, &:focus {
background-color: $button-hover-background-color;
color: $button-hover-text-color;
}
&:active {
background-color: $button-active-background-color;
color: $button-active-text-color;
}
}
input[type="submit"].other {
background-color: $button-background-color;
color: $button-text-color;
font-size: 12px;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px;
@include for-mobile {
display: block;
width: 100%;
}
&:hover, &:focus {
background-color: $button-hover-background-color;
color: $button-hover-text-color;
}
&:active {
background-color: $button-active-background-color;
color: $button-active-text-color;
}
}
.button {
vertical-align: middle;
margin-left: 10px;
margin-bottom: 5px;
box-sizing: border-box;
@include for-mobile {
margin-left: 0;
display: block;
width: 100%;
}
}
}
}
/*
@import "globals";
.admin-view {
#content > ul, #content > h1 + p + ul {
background: $content-background-color;
border-radius: 4px;
box-shadow: 0 2px 0 0 $content-border2-color;
&, ul {
list-style-type: none;
margin: 0;
padding: 0;
li {
list-style: disc;
line-height: 1.8;
}
}
ul {
margin-left: 20px;
}
> li {
padding: 8px;
border-bottom: 1px solid $content-border-color;
font-size: 13px;
list-style: none;
&:last-child {
border-bottom: 0;
}
}
}
input[type="submit"] {
background-color: $danger-button-background-color;
color: $danger-button-text-color;
font-size: 12px;
font-weight: lighter;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px;
@include for-mobile {
display: block;
width: 100%;
}
&:hover, &:focus {
background-color: $button-hover-background-color;
color: $button-hover-text-color;
}
&:active {
background-color: $button-active-background-color;
color: $button-active-text-color;
}
}
}
*/

@ -171,4 +171,5 @@ body {
@import "dashboard";
@import "delete-confirmation";
@import "invite-waiter";
@import "admin-view";
@import "login";

@ -9622,6 +9622,465 @@ ul.actionlist li {
.fill_width {
width: 100% !important; }
@keyframes spin {
100% {
transform: rotate(360deg); } }
.admin-view #content > ul, .admin-view #content > h1 + p + ul {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 0 0 #cceae4; }
.admin-view #content > ul, .admin-view #content > ul ul, .admin-view #content > h1 + p + ul, .admin-view #content > h1 + p + ul ul {
list-style-type: none;
margin: 0;
padding: 0; }
.admin-view #content > ul li, .admin-view #content > ul ul li, .admin-view #content > h1 + p + ul li, .admin-view #content > h1 + p + ul ul li {
list-style: disc;
line-height: 1.8; }
.admin-view #content > ul ul, .admin-view #content > h1 + p + ul ul {
margin-left: 20px; }
.admin-view #content > ul > li, .admin-view #content > h1 + p + ul > li {
padding: 8px;
border-bottom: 1px solid #f5f3f4;
font-size: 13px;
list-style: none; }
.admin-view #content > ul > li:last-child, .admin-view #content > h1 + p + ul > li:last-child {
border-bottom: 0; }
.admin-view #content form {
margin-top: 20px; }
.admin-view #content form input[type="submit"].danger {
background-color: #c14747;
color: #fff;
font-size: 12px;
font-weight: lighter;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px; }
@media only screen and (max-width: 960px) {
.admin-view #content form input[type="submit"].danger {
display: block;
width: 100%; } }
.admin-view #content form input[type="submit"].danger:hover, .admin-view #content form input[type="submit"].danger:focus {
background-color: #7fb1dc;
color: #fff; }
.admin-view #content form input[type="submit"].danger:active {
background-color: #4f8580;
color: #fff; }
.admin-view #content form input[type="submit"].confirm {
background-color: #44b78b;
color: #fff;
font-size: 12px;
font-weight: lighter;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px; }
@media only screen and (max-width: 960px) {
.admin-view #content form input[type="submit"].confirm {
display: block;
width: 100%; } }
.admin-view #content form input[type="submit"].confirm:hover, .admin-view #content form input[type="submit"].confirm:focus {
background-color: #7fb1dc;
color: #fff; }
.admin-view #content form input[type="submit"].confirm:active {
background-color: #4f8580;
color: #fff; }
.admin-view #content form input[type="submit"].other {
background-color: #cceae4;
color: #4f8580;
font-size: 12px;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px; }
@media only screen and (max-width: 960px) {
.admin-view #content form input[type="submit"].other {
display: block;
width: 100%; } }
.admin-view #content form input[type="submit"].other:hover, .admin-view #content form input[type="submit"].other:focus {
background-color: #7fb1dc;
color: #fff; }
.admin-view #content form input[type="submit"].other:active {
background-color: #4f8580;
color: #fff; }
.admin-view #content form .button {
vertical-align: middle;
margin-left: 10px;
margin-bottom: 5px;
box-sizing: border-box; }
@media only screen and (max-width: 960px) {
.admin-view #content form .button {
margin-left: 0;
display: block;
width: 100%; } }
/*
@import "globals";
.admin-view {
#content > ul, #content > h1 + p + ul {
background: $content-background-color;
border-radius: 4px;
box-shadow: 0 2px 0 0 $content-border2-color;
&, ul {
list-style-type: none;
margin: 0;
padding: 0;
li {
list-style: disc;
line-height: 1.8;
}
}
ul {
margin-left: 20px;
}
> li {
padding: 8px;
border-bottom: 1px solid $content-border-color;
font-size: 13px;
list-style: none;
&:last-child {
border-bottom: 0;
}
}
}
input[type="submit"] {
background-color: $danger-button-background-color;
color: $danger-button-text-color;
font-size: 12px;
font-weight: lighter;
padding: 0 20px;
text-transform: uppercase;
vertical-align: middle;
margin-bottom: 5px;
@include for-mobile {
display: block;
width: 100%;
}
&:hover, &:focus {
background-color: $button-hover-background-color;
color: $button-hover-text-color;
}
&:active {
background-color: $button-active-background-color;
color: $button-active-text-color;
}
}
}
*/
/*
* Default variable values
* Create separate themes/theme/_variables.scss to override these variables
*/
/*
* General
*/
/*
* Sidebar
*/
/*
* Top
*/
/*
* Content
*/
/*
* Buttons
*/
/*
* Inputs
*/
/*
* Messages
*/
/*
* Login
*/
/*
* jQuery UI
*/
/*
* Charts
*/
.hidden {
display: none; }
.clear-list, .dashboard-item-content ul:not(.inline), .dashboard-item-content ul.inline {
margin: 0;
padding: 0;
list-style: none; }
.fl {
float: left; }
.fr {
float: right; }
.cf:before, .cf:after {
content: "";
display: table; }
.cf:after {
clear: both; }
.p10 {
padding: 10px; }
.p20 {
padding: 20px; }
.p30 {
padding: 30px; }
.p40 {
padding: 40px; }
.p50 {
padding: 50px; }
.p60 {
padding: 60px; }
.p70 {
padding: 70px; }
.p80 {
padding: 80px; }
.pt10 {
padding-top: 10px; }
.pt20 {
padding-top: 20px; }
.pt30 {
padding-top: 30px; }
.pt40 {
padding-top: 40px; }
.pt50 {
padding-top: 50px; }
.pt60 {
padding-top: 60px; }
.pt70 {
padding-top: 70px; }
.pt80 {
padding-top: 80px; }
.pr10 {
padding-right: 10px; }
.pr20 {
padding-right: 20px; }
.pr30 {
padding-right: 30px; }
.pr40 {
padding-right: 40px; }
.pr50 {
padding-right: 50px; }
.pr60 {
padding-right: 60px; }
.pr70 {
padding-right: 70px; }
.pr80 {
padding-right: 80px; }
.pb10 {
padding-bottom: 10px; }
.pb20 {
padding-bottom: 20px; }
.pb30 {
padding-bottom: 30px; }
.pb40 {
padding-bottom: 40px; }
.pb50 {
padding-bottom: 50px; }
.pb60 {
padding-bottom: 60px; }
.pb70 {
padding-bottom: 70px; }
.pb80 {
padding-bottom: 80px; }
.pl10 {
padding-left: 10px; }
.pl20 {
padding-left: 20px; }
.pl30 {
padding-left: 30px; }
.pl40 {
padding-left: 40px; }
.pl50 {
padding-left: 50px; }
.pl60 {
padding-left: 60px; }
.pl70 {
padding-left: 70px; }
.pl80 {
padding-left: 80px; }
.m10 {
margin: 10px; }
.m20 {
margin: 20px; }
.m30 {
margin: 30px; }
.m40 {
margin: 40px; }
.m50 {
margin: 50px; }
.m60 {
margin: 60px; }
.m70 {
margin: 70px; }
.m80 {
margin: 80px; }
.mt10 {
margin-top: 10px; }
.mt20 {
margin-top: 20px; }
.mt30 {
margin-top: 30px; }
.mt40 {
margin-top: 40px; }
.mt50 {
margin-top: 50px; }
.mt60 {
margin-top: 60px; }
.mt70 {
margin-top: 70px; }
.mt80 {
margin-top: 80px; }
.mr10 {
margin-right: 10px; }
.mr20 {
margin-right: 20px; }
.mr30 {
margin-right: 30px; }
.mr40 {
margin-right: 40px; }
.mr50 {
margin-right: 50px; }
.mr60 {
margin-right: 60px; }
.mr70 {
margin-right: 70px; }
.mr80 {
margin-right: 80px; }
.mb10 {
margin-bottom: 10px; }
.mb20 {
margin-bottom: 20px; }
.mb30 {
margin-bottom: 30px; }
.mb40 {
margin-bottom: 40px; }
.mb50 {
margin-bottom: 50px; }
.mb60 {
margin-bottom: 60px; }
.mb70 {
margin-bottom: 70px; }
.mb80 {
margin-bottom: 80px; }
.ml10 {
margin-left: 10px; }
.ml20 {
margin-left: 20px; }
.ml30 {
margin-left: 30px; }
.ml40 {
margin-left: 40px; }
.ml50 {
margin-left: 50px; }
.ml60 {
margin-left: 60px; }
.ml70 {
margin-left: 70px; }
.ml80 {
margin-left: 80px; }
.pos_rel {
position: relative; }
.pos_abs {
position: absolute; }
.fill_width {
width: 100% !important; }
@keyframes spin {
100% {
transform: rotate(360deg); } }

File diff suppressed because one or more lines are too long

@ -0,0 +1,13 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls %}
{% block object-tools-items %}
<li>
{% url opts|admin_urlname:'submit' original.pk|admin_urlquote as invite_url %}
<a class="historylink" href="{% add_preserved_filters invite_url %}">{% trans 'Unconfirm' %}</a>
</li>
{{block.super}}
{% endblock %}

@ -3,9 +3,32 @@
{% block object-tools-items %}
<script>
function requestWithCurrentURL(path) {
var xpath = path + "?redirectTo=" + window.location.href;
location.href = xpath;
}
</script>
{% comment %}
/de/kompass/finance/statementsubmitted/1/reduce_transactions/?redirectTo=http://localhost:8000/de/kompass/finance/statementsubmitted/1/change/
{% endcomment %}
<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>
{% comment %}
<form method="post" action=""
onsubmit="";>
{% csrf_token %}
<input type="submit" name="reduce_transactions" value="{% trans 'Reduce transactions' %}">
<input type="hidden" name="path" value="{{request.path}}">
</form>
<a class="historylink" href="{% add_preserved_filters invite_url %}">{% trans 'Reduce transactions' %}</a>
{% endcomment %}
</li>
<li>
{% url opts|admin_urlname:'overview' original.pk|admin_urlquote as invite_url %}

Loading…
Cancel
Save