714 lines
20 KiB
Python
714 lines
20 KiB
Python
import logging
|
|
from datetime import date, datetime
|
|
|
|
from django.conf import settings
|
|
from django.contrib import admin, messages
|
|
from django.http import HttpResponseRedirect
|
|
from django.utils.html import format_html
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.translation import ngettext
|
|
from segno import helpers
|
|
|
|
from posts.models import FetMeeting
|
|
|
|
from .forms import (
|
|
BankDataAdminForm,
|
|
BillAdminForm,
|
|
BillInlineForm,
|
|
FeeAdminForm,
|
|
ResolutionAdminForm,
|
|
WirefAdminForm,
|
|
)
|
|
from .models import BankData, Bill, Fee, Resolution, Wiref
|
|
from .utils import generate_fee_pdf, generate_pdf
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BillPeriodeFilter(admin.SimpleListFilter):
|
|
title = "Periode"
|
|
parameter_name = "period"
|
|
|
|
__lst = ()
|
|
|
|
def choices(self, changelist):
|
|
# Current period.
|
|
yield {
|
|
"selected": self.value() is None,
|
|
"query_string": changelist.get_query_string(remove=[self.parameter_name]),
|
|
"display": self.__lst[0][1],
|
|
}
|
|
|
|
# Add all other periods.
|
|
for lookup, title in self.__lst[1:]:
|
|
yield {
|
|
"selected": self.value() == str(lookup),
|
|
"query_string": changelist.get_query_string({self.parameter_name: lookup}),
|
|
"display": title,
|
|
}
|
|
|
|
# 'All' choice.
|
|
yield {
|
|
"selected": self.value() == "All",
|
|
"query_string": changelist.get_query_string({self.parameter_name: "All"}),
|
|
"display": _("All"),
|
|
}
|
|
|
|
def lookups(self, request, model_admin):
|
|
qs = model_admin.get_queryset(request).order_by("-date")
|
|
if not qs.exists():
|
|
return None
|
|
|
|
count = 0
|
|
total_count = qs.count()
|
|
|
|
# Get first period.
|
|
start_year = qs.first().date.year
|
|
|
|
# Check if date of first bill is in first half of year. If yes, start of period is the
|
|
# year before.
|
|
if qs.first().date < date(start_year, 7, 1):
|
|
start_year -= 1
|
|
|
|
start_date = date(start_year, 7, 1)
|
|
end_date = date(start_year + 1, 6, 30)
|
|
entry_qs = qs.filter(date__range=(start_date, end_date))
|
|
|
|
# Go through all bills to get the periods.
|
|
while count != total_count:
|
|
# Set period if a bill exists.
|
|
if entry_qs.exists():
|
|
self.__lst += (
|
|
(
|
|
f"{start_year}-{start_year + 1}",
|
|
f"Periode {start_year}-{start_year + 1}",
|
|
),
|
|
)
|
|
count += entry_qs.count()
|
|
|
|
# Get bills from next period.
|
|
start_year -= 1
|
|
start_date = date(start_year, 7, 1)
|
|
end_date = date(start_year + 1, 6, 30)
|
|
entry_qs = qs.filter(date__range=(start_date, end_date))
|
|
|
|
# If you are searching bills from 90's, something went wrong.
|
|
if start_year < 2000:
|
|
logger.info(
|
|
"Something went wrong while counting. Count: %s. Totalcounter: %s",
|
|
count,
|
|
total_count,
|
|
)
|
|
break
|
|
|
|
return self.__lst
|
|
|
|
def queryset(self, request, queryset):
|
|
qs = queryset
|
|
|
|
if self.value():
|
|
try:
|
|
period = datetime.strptime(self.value()[:4], "%Y") # noqa: DTZ007
|
|
except Exception:
|
|
# If choice is 'All', return all bills.
|
|
qs = queryset
|
|
else:
|
|
# Return bills from specific old period.
|
|
start_year = period.year
|
|
start_date = date(start_year, 7, 1)
|
|
end_date = date(start_year + 1, 6, 30)
|
|
qs = queryset.filter(date__range=(start_date, end_date))
|
|
|
|
# Return bills from current period.
|
|
else:
|
|
tmp_qs = queryset.order_by("-date")
|
|
|
|
# Get first period.
|
|
start_year = tmp_qs.first().date.year
|
|
|
|
# Check if date of first bill is in first half of year. If yes, start of period is the
|
|
# year before.
|
|
if tmp_qs.first().date < date(start_year, 7, 1):
|
|
start_year -= 1
|
|
|
|
start_date = date(start_year, 7, 1)
|
|
end_date = date(start_year + 1, 6, 30)
|
|
qs = queryset.filter(date__range=(start_date, end_date))
|
|
|
|
return qs
|
|
|
|
|
|
class BillInline(admin.TabularInline):
|
|
form = BillInlineForm
|
|
model = Bill
|
|
|
|
can_delete = False
|
|
extra = 0
|
|
max_num = 0
|
|
readonly_fields = ("purpose", "amount", "file_field", "affiliation")
|
|
show_change_link = True
|
|
|
|
|
|
@admin.register(BankData)
|
|
class BankDataAdmin(admin.ModelAdmin):
|
|
form = BankDataAdminForm
|
|
model = BankData
|
|
|
|
inlines = (BillInline,)
|
|
|
|
list_display = [
|
|
"name",
|
|
"iban",
|
|
"bic",
|
|
"address",
|
|
"is_disabled",
|
|
]
|
|
|
|
ordering = ["name"]
|
|
|
|
def add_view(self, request, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().add_view(
|
|
request,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().change_view(
|
|
request,
|
|
object_id,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
|
|
@admin.register(Bill)
|
|
class BillAdmin(admin.ModelAdmin):
|
|
form = BillAdminForm
|
|
model = Bill
|
|
|
|
list_display = [
|
|
"id",
|
|
"only_digital",
|
|
"amount",
|
|
"purpose",
|
|
"resolution",
|
|
"status_colored",
|
|
"bill_creator",
|
|
"affiliation",
|
|
]
|
|
|
|
actions = ["make_cleared", "make_finished"]
|
|
autocomplete_fields = ["resolution"]
|
|
list_filter = ["status", "affiliation", "payer", BillPeriodeFilter]
|
|
search_fields = ["purpose", "bankdata__name"]
|
|
show_facets = admin.ShowFacets.ALWAYS
|
|
ordering = ["-id"]
|
|
|
|
readonly_fields = [
|
|
"get_bankdata_name",
|
|
"get_bankdata_iban",
|
|
"get_bankdata_bic",
|
|
"get_qrcode",
|
|
]
|
|
fieldsets = (
|
|
(
|
|
None,
|
|
{
|
|
"fields": (
|
|
"bill_creator",
|
|
"resolution",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Bankdaten",
|
|
{
|
|
"fields": (
|
|
"payer",
|
|
"bankdata",
|
|
("get_bankdata_name", "get_bankdata_iban", "get_bankdata_bic"),
|
|
"get_qrcode",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Rechnung",
|
|
{
|
|
"fields": (
|
|
"affiliation",
|
|
"date",
|
|
"invoice",
|
|
"purpose",
|
|
"amount",
|
|
"only_digital",
|
|
"file_field",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Sonstiges",
|
|
{
|
|
"fields": (
|
|
"comment",
|
|
"wiref",
|
|
"status",
|
|
),
|
|
},
|
|
),
|
|
)
|
|
|
|
def add_view(self, request, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().add_view(
|
|
request,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().change_view(
|
|
request,
|
|
object_id,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
obj.author = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
@admin.display(description="Kontoinhaber:in")
|
|
def get_bankdata_name(self, obj):
|
|
try:
|
|
tmp = BankData.objects.get(id=obj.bankdata.id)
|
|
except Exception:
|
|
return "-"
|
|
|
|
return f"{tmp.name}"
|
|
|
|
@admin.display(description="IBAN")
|
|
def get_bankdata_iban(self, obj):
|
|
try:
|
|
tmp = BankData.objects.get(id=obj.bankdata.id)
|
|
except Exception:
|
|
return "-"
|
|
|
|
return f"{tmp.iban}"
|
|
|
|
@admin.display(description="BIC")
|
|
def get_bankdata_bic(self, obj):
|
|
try:
|
|
tmp = BankData.objects.get(id=obj.bankdata.id)
|
|
except Exception:
|
|
return "-"
|
|
|
|
return f"{tmp.bic}"
|
|
|
|
@admin.display(description="QR Code")
|
|
def get_qrcode(self, obj):
|
|
if obj.status != Bill.Status.CLEARED:
|
|
return "-"
|
|
|
|
try:
|
|
tmp = BankData.objects.get(id=obj.bankdata.id)
|
|
except Exception:
|
|
return "-"
|
|
|
|
try:
|
|
qrcode = helpers.make_epc_qr(
|
|
name=tmp.name,
|
|
iban=tmp.iban,
|
|
amount=obj.amount,
|
|
text=obj.purpose,
|
|
bic=tmp.bic,
|
|
encoding="utf-8",
|
|
)
|
|
except Exception:
|
|
return "Daten für QR Code ungültig"
|
|
|
|
uri = qrcode.png_data_uri(scale=3.0)
|
|
return format_html('<img src="{}">', uri)
|
|
|
|
@admin.display(description="Status")
|
|
def status_colored(self, obj):
|
|
# TODO: if there is a status without color, set nothing.
|
|
colors = {
|
|
Bill.Status.SUBMITTED: "red",
|
|
Bill.Status.CLEARED: "darkorange",
|
|
Bill.Status.FINISHED: "green",
|
|
Bill.Status.INCOMPLETED: "blue",
|
|
}
|
|
return format_html(
|
|
'<b style="background:{color};">{status}</b>',
|
|
color=colors[obj.status],
|
|
status=obj.get_status_display(),
|
|
)
|
|
|
|
@admin.action(description="Als 'Für Überweisung freigegeben' markieren.")
|
|
def make_cleared(self, request, queryset):
|
|
updated = queryset.update(status=Bill.Status.CLEARED)
|
|
self.message_user(
|
|
request,
|
|
ngettext(
|
|
"%d Rechnung wurde als 'Für Überweisung freigegeben' markiert.",
|
|
"%d Rechnungen wurden als 'Für Überweisung freigegeben' markiert.",
|
|
updated,
|
|
)
|
|
% updated,
|
|
messages.SUCCESS,
|
|
)
|
|
|
|
@admin.action(description="Als 'Abgeschlossen / Überwiesen' markieren.")
|
|
def make_finished(self, request, queryset):
|
|
updated = queryset.update(status=Bill.Status.FINISHED)
|
|
self.message_user(
|
|
request,
|
|
ngettext(
|
|
"%d Rechnung wurde als 'Abgeschlossen / Überwiesen' markiert.",
|
|
"%d Rechnungen wurden als 'Abgeschlossen / Überwiesen' markiert.",
|
|
updated,
|
|
)
|
|
% updated,
|
|
messages.SUCCESS,
|
|
)
|
|
|
|
|
|
@admin.register(Fee)
|
|
class FeeAdmin(admin.ModelAdmin):
|
|
form = FeeAdminForm
|
|
model = Fee
|
|
|
|
list_display = ["id", "amount", "job", "fix_name_desc", "status_colored"]
|
|
|
|
list_filter = ["status"]
|
|
show_facets = admin.ShowFacets.ALWAYS
|
|
ordering = ["-id"]
|
|
|
|
readonly_fields = [
|
|
"address",
|
|
"get_qrcode",
|
|
]
|
|
fieldsets = (
|
|
(
|
|
None,
|
|
{
|
|
"fields": (
|
|
"fee_creator",
|
|
"bankdata",
|
|
"address",
|
|
"get_qrcode",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Tätigkeit",
|
|
{
|
|
"fields": (
|
|
"job",
|
|
"date_start",
|
|
"date_end",
|
|
"amount",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Sonstiges",
|
|
{
|
|
"fields": (
|
|
"comment",
|
|
"status",
|
|
"file_field",
|
|
),
|
|
},
|
|
),
|
|
)
|
|
|
|
def add_view(self, request, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().add_view(
|
|
request,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
extra_context["generate_fee_pdf"] = True
|
|
return super().change_view(
|
|
request,
|
|
object_id,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def response_change(self, request, obj):
|
|
if "_generate_fee_pdf" in request.POST:
|
|
if generate_fee_pdf(obj):
|
|
self.message_user(
|
|
request,
|
|
"Neue Honorarnote wurde generiert.",
|
|
messages.SUCCESS,
|
|
)
|
|
else:
|
|
self.message_user(
|
|
request,
|
|
(
|
|
"Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf "
|
|
"'Eingereicht' gesetzt ist."
|
|
),
|
|
messages.WARNING,
|
|
)
|
|
return HttpResponseRedirect(".")
|
|
return super().response_change(request, obj)
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
# set status to submitted, if a file exists and status is opened.
|
|
if (
|
|
change
|
|
and obj.file_field
|
|
and obj.status == Fee.Status.SUBMITTED
|
|
and "_generate_fee_pdf" not in request.POST
|
|
):
|
|
obj.status = Fee.Status.APPROVED
|
|
super().save_model(request, obj, form, change)
|
|
|
|
@admin.display(description="Adresse")
|
|
def address(self, obj):
|
|
return obj.bankdata.address
|
|
|
|
@admin.display(description="QR Code")
|
|
def get_qrcode(self, obj):
|
|
# QR Code is only set if status is approved.
|
|
if obj.status != Fee.Status.APPROVED:
|
|
return "-"
|
|
|
|
try:
|
|
qrcode = helpers.make_epc_qr(
|
|
name=obj.bankdata.name,
|
|
iban=obj.bankdata.iban,
|
|
amount=obj.amount,
|
|
text=f"Honorarnote Nr.{obj.id}",
|
|
bic=obj.bankdata.bic,
|
|
encoding="utf-8",
|
|
)
|
|
except Exception:
|
|
return "Daten für QR Code ungültig"
|
|
|
|
uri = qrcode.png_data_uri(scale=3.0)
|
|
return format_html('<img src="{}">', uri)
|
|
|
|
@admin.display(description="Name")
|
|
def fix_name_desc(self, obj):
|
|
return obj.bankdata.name
|
|
|
|
@admin.display(description="Status")
|
|
def status_colored(self, obj):
|
|
# TODO: if there is a status without color, set nothing.
|
|
colors = {
|
|
Fee.Status.SUBMITTED: "red",
|
|
Fee.Status.APPROVED: "darkorange",
|
|
Fee.Status.PAYOUT: "green",
|
|
Fee.Status.CLEARED: "DarkMagenta",
|
|
}
|
|
return format_html(
|
|
'<b style="background:{color};">{status}</b>',
|
|
color=colors[obj.status],
|
|
status=obj.get_status_display(),
|
|
)
|
|
|
|
|
|
@admin.register(Resolution)
|
|
class ResolutionAdmin(admin.ModelAdmin):
|
|
form = ResolutionAdminForm
|
|
model = Resolution
|
|
|
|
inlines = (BillInline,)
|
|
|
|
list_display = [
|
|
"name",
|
|
"id",
|
|
"is_visible",
|
|
"total",
|
|
]
|
|
|
|
ordering = ["-id"]
|
|
search_fields = ["name", "id"]
|
|
|
|
readonly_fields = [
|
|
"fetmeeting",
|
|
]
|
|
fieldsets = (
|
|
(
|
|
None,
|
|
{
|
|
"fields": (
|
|
"name",
|
|
"id",
|
|
"date",
|
|
"option",
|
|
"is_visible",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Budget",
|
|
{
|
|
"fields": (
|
|
"budget",
|
|
"total",
|
|
"budget_remaining",
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"Abstimmung",
|
|
{
|
|
"classes": ["collapse"],
|
|
"fields": (
|
|
"voting",
|
|
"voting_text",
|
|
"fetmeeting",
|
|
),
|
|
},
|
|
),
|
|
)
|
|
|
|
def add_view(self, request, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().add_view(
|
|
request,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().change_view(
|
|
request,
|
|
object_id,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
@admin.display(description="Gesamtsumme (EUR)")
|
|
def total(self, obj):
|
|
total = 0
|
|
bills = Bill.objects.filter(resolution=obj)
|
|
for elem in bills:
|
|
total += elem.amount
|
|
|
|
return f"{total}"
|
|
|
|
@admin.display(description="Fachschaftssitzung")
|
|
def fetmeeting(self, obj):
|
|
fetmeeting = FetMeeting.objects.get_queryset().filter(date=obj.date).first()
|
|
|
|
if fetmeeting is not None:
|
|
link = f"https://{settings.HOST_NAME}/posts/{fetmeeting.slug}/"
|
|
return format_html('<a href="{}" target="_blank">Link zur Fachschaftssitzung</a>', link)
|
|
|
|
return format_html("-")
|
|
|
|
|
|
@admin.register(Wiref)
|
|
class WirefAdmin(admin.ModelAdmin):
|
|
form = WirefAdminForm
|
|
model = Wiref
|
|
|
|
inlines = (BillInline,)
|
|
|
|
list_display = [
|
|
"wiref_id",
|
|
"status",
|
|
"file_field",
|
|
"total",
|
|
]
|
|
|
|
actions = ["make_cleared", "make_finished"]
|
|
list_filter = ["status"]
|
|
ordering = ["-wiref_id"]
|
|
|
|
def add_view(self, request, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
return super().add_view(
|
|
request,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
|
extra_context = extra_context or {}
|
|
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
|
extra_context["generate_pdf"] = True
|
|
return super().change_view(
|
|
request,
|
|
object_id,
|
|
form_url,
|
|
extra_context=extra_context,
|
|
)
|
|
|
|
def response_change(self, request, obj):
|
|
if "_generate_pdf" in request.POST:
|
|
if generate_pdf(obj) is True:
|
|
self.message_user(
|
|
request,
|
|
"Neues Wiref Formular wurde generiert.",
|
|
messages.SUCCESS,
|
|
)
|
|
else:
|
|
self.message_user(
|
|
request,
|
|
"Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf "
|
|
"'Offen' gesetzt ist.",
|
|
messages.WARNING,
|
|
)
|
|
return HttpResponseRedirect(".")
|
|
return super().response_change(request, obj)
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
# set status to submitted, if a file exists and status is opened.
|
|
if (
|
|
change is True
|
|
and obj.file_field != ""
|
|
and obj.status == Wiref.Status.OPENED
|
|
and "_generate_pdf" not in request.POST
|
|
):
|
|
obj.status = Wiref.Status.SUBMITTED
|
|
super().save_model(request, obj, form, change)
|
|
|
|
@admin.display(description="Gesamtsumme (EUR)")
|
|
def total(self, obj):
|
|
total = 0
|
|
bills = Bill.objects.filter(wiref=obj)
|
|
for elem in bills:
|
|
total += elem.amount
|
|
|
|
return f"{total}"
|
|
|
|
@admin.action(description="Als 'Überwiesen' markieren.")
|
|
def make_finished(self, request, queryset):
|
|
updated = queryset.update(status=Wiref.Status.TRANSFERRED)
|
|
self.message_user(
|
|
request,
|
|
ngettext(
|
|
"%d Wiref Formular wurde als 'Überwiesen' markiert.",
|
|
"%d Wiref Formulare wurden als 'Überwiesen' markiert.",
|
|
updated,
|
|
)
|
|
% updated,
|
|
messages.SUCCESS,
|
|
)
|