This commit is contained in:
2025-02-07 13:26:47 +01:00
parent f6ffcd37da
commit 58b74bdfab
14 changed files with 8615 additions and 45 deletions

View File

@@ -78,8 +78,9 @@ def get_app_list(self, request, app_label=None):
ordering = { ordering = {
"Rechnungen": 1, "Rechnungen": 1,
"Wiref Formulare": 2, "Wiref Formulare": 2,
"Beschlüsse": 3, "Honorare": 3,
"Bankdaten": 4, "Beschlüsse": 4,
"Bankdaten": 5,
} }
app["models"].sort(key=lambda x: ordering[x["name"]]) app["models"].sort(key=lambda x: ordering[x["name"]])

View File

@@ -15,11 +15,12 @@ from .forms import (
BankDataAdminForm, BankDataAdminForm,
BillAdminForm, BillAdminForm,
BillInlineForm, BillInlineForm,
FeeAdminForm,
ResolutionAdminForm, ResolutionAdminForm,
WirefAdminForm, WirefAdminForm,
) )
from .models import BankData, Bill, Resolution, Wiref from .models import BankData, Bill, Budget, Fee, Resolution, Wiref
from .utils import generate_pdf from .utils import generate_fee_pdf, generate_pdf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -157,6 +158,7 @@ class BankDataAdmin(admin.ModelAdmin):
"name", "name",
"iban", "iban",
"bic", "bic",
"address",
"is_disabled", "is_disabled",
] ]
@@ -375,6 +377,152 @@ class BillAdmin(admin.ModelAdmin):
) )
@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 File konnte nicht generiert werden, weil 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) @admin.register(Resolution)
class ResolutionAdmin(admin.ModelAdmin): class ResolutionAdmin(admin.ModelAdmin):
form = ResolutionAdminForm form = ResolutionAdminForm

View File

@@ -8,7 +8,7 @@ from django.forms import DateInput
from members.models import Member from members.models import Member
from .models import BankData, Bill, Resolution, Wiref from .models import BankData, Bill, Fee, Resolution, Wiref
class DateInput(DateInput): class DateInput(DateInput):
@@ -19,9 +19,9 @@ class BankDataForm(forms.ModelForm):
class Meta: class Meta:
model = BankData model = BankData
fields = ["iban", "bic", "name"] fields = ["iban", "bic", "name", "address"]
labels = {"iban": "IBAN", "bic": "BIC", "name": "Kontoinhaber:in"} labels = {"iban": "IBAN", "bic": "BIC", "name": "Kontoinhaber:in", "address": "Adresse"}
def get_cleaned_data(cleaned_data): def get_cleaned_data(cleaned_data):
@@ -283,6 +283,171 @@ class BillUpdateForm(forms.ModelForm):
return get_cleaned_data(super().clean()) return get_cleaned_data(super().clean())
class FeeCreateForm(forms.ModelForm):
# Bank data
name_text = forms.CharField(required=True, label="Kontoinhaber:in", initial="", max_length=128)
iban_text = forms.CharField(required=True, label="IBAN", initial="", max_length=34)
bic_text = forms.CharField(required=True, label="BIC", initial="", max_length=11)
address_text = forms.CharField(
required=True, widget=forms.Textarea, label="Adresse", initial=""
)
saving = forms.BooleanField(
required=False,
label="Bankdaten für die nächsten Rechnungen speichern.",
initial=False,
)
# Conformation
conformation = forms.BooleanField(
required=True,
label=(
"Hiermit bestätige ich, dass mir die relevanten rechtlichen und steuerlichen "
"Bestimmungen im Zusammenhang mit Honorarnoten bekannt sind. Ich verpflichte "
"mich, diese in Übereinstimmung mit den geltenden steuerlichen Vorschriften "
"ordnungsgemäß zu melden."
),
initial=False,
)
class Meta:
model = Fee
fields = [
"fee_creator",
"job",
"date_start",
"date_end",
"amount",
"comment",
]
help_texts = {
"date_end": "Bei einer leeren Eingabe Eingabe wird automatisch das Startdatum gesetzt."
}
labels = {
"job": "Tätigkeitsbeschreibung",
}
widgets = {
"date_start": DateInput(format=("%Y-%m-%d")),
"date_end": DateInput(format=("%Y-%m-%d")),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop("user") if "user" in kwargs else None
super().__init__(*args, **kwargs) # to get the self.fields set
member = Member.objects.get(username=user.username)
self.fields["fee_creator"].initial = member
self.fields["fee_creator"].disabled = True
self.fields["fee_creator"].required = True
self.fields["date_end"].required = False
self.fields["address_text"].placeholder = "Straße\nPLZ und Ort"
# Bank data fields
bank_data = BankData.objects.filter(
Q(bankdata_creator=member) & Q(is_disabled=False),
).first()
if bank_data:
self.fields["name_text"].initial = bank_data.name
self.fields["iban_text"].initial = bank_data.iban
self.fields["bic_text"].initial = bank_data.bic
self.fields["address_text"].initial = bank_data.address
self.fields["saving"].initial = True
class FeeUpdateForm(forms.ModelForm):
# Bank data
name_text = forms.CharField(required=False, label="Kontoinhaber:in", initial="", max_length=128)
iban_text = forms.CharField(required=False, label="IBAN", initial="", max_length=34)
bic_text = forms.CharField(required=False, label="BIC", initial="", max_length=11)
address_text = forms.CharField(
required=False, widget=forms.Textarea, label="Adresse", initial=""
)
saving = forms.BooleanField(
required=False,
label="Bankdaten für die nächsten Rechnungen speichern.",
initial=False,
)
class Meta:
model = Fee
fields = [
"fee_creator",
"job",
"date_start",
"date_end",
"amount",
"status",
"comment",
]
labels = {
"job": "Tätigkeitsbeschreibung",
}
widgets = {
"date_start": DateInput(format=("%Y-%m-%d")),
"date_end": DateInput(format=("%Y-%m-%d")),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["fee_creator"].initial = kwargs["instance"].fee_creator
self.fields["fee_creator"].disabled = True
self.fields["fee_creator"].required = True
self.fields["status"].disabled = True
# Config for textarea of job. Calc rows for a better view.
if (rows := kwargs["instance"].job.count("\n") + 1) < 3:
rows = 3
self.fields["job"].rows = rows
self.fields["job"].disabled = True
self.fields["date_start"].disabled = True
self.fields["date_end"].disabled = True
self.fields["amount"].disabled = True
# Bank data fields
if kwargs["instance"].bankdata:
self.fields["name_text"].initial = kwargs["instance"].bankdata.name
self.fields["name_text"].required = True
self.fields["iban_text"].initial = kwargs["instance"].bankdata.iban
self.fields["iban_text"].required = True
self.fields["bic_text"].initial = kwargs["instance"].bankdata.bic
self.fields["bic_text"].required = True
self.fields["address_text"].initial = kwargs["instance"].bankdata.address
self.fields["saving"].initial = not kwargs["instance"].bankdata.is_disabled
self.fields["name_text"].disabled = True
self.fields["iban_text"].disabled = True
self.fields["bic_text"].disabled = True
self.fields["address_text"].disabled = True
self.fields["saving"].disabled = True
# Config for textarea of comment. Calc rows for a better view.
rows = kwargs["instance"].comment.count("\n") + 1
self.fields["comment"].rows = rows
# Comment disabled when bill is cleared or finished
if kwargs["instance"].status != Bill.Status.SUBMITTED:
self.fields["comment"].disabled = True
self.fields["comment"].autofocus = True
class ResolutionCreateForm(forms.ModelForm): class ResolutionCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Resolution model = Resolution
@@ -439,6 +604,28 @@ class BillAdminForm(forms.ModelForm):
self.fields["wiref"].queryset = qs.order_by("-wiref_id") self.fields["wiref"].queryset = qs.order_by("-wiref_id")
class FeeAdminForm(forms.ModelForm):
class Meta:
model = Fee
fields = "__all__"
help_texts = {
"date_end": "Bei einer leeren Eingabe Eingabe wird automatisch das Startdatum gesetzt."
}
widgets = {
"date": DateInput(format=("%Y-%m-%d")),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["bankdata"].required = True
self.fields["date_end"].required = False
class ResolutionAdminForm(forms.ModelForm): class ResolutionAdminForm(forms.ModelForm):
total = forms.CharField() total = forms.CharField()
budget_remaining = forms.CharField() budget_remaining = forms.CharField()

View File

@@ -1,3 +1,5 @@
from pathlib import Path
from django.core.validators import FileExtensionValidator, ValidationError from django.core.validators import FileExtensionValidator, ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@@ -17,6 +19,8 @@ class BankData(models.Model):
iban = models.CharField(max_length=34, verbose_name="IBAN") iban = models.CharField(max_length=34, verbose_name="IBAN")
bic = models.CharField(max_length=11, verbose_name="BIC") bic = models.CharField(max_length=11, verbose_name="BIC")
address = models.TextField(blank=True, verbose_name="Adresse")
is_disabled = models.BooleanField(default=False, verbose_name="deaktiviert") is_disabled = models.BooleanField(default=False, verbose_name="deaktiviert")
class Meta: class Meta:
@@ -34,6 +38,71 @@ class BankData(models.Model):
self.bic = self.bic.replace(" ", "") self.bic = self.bic.replace(" ", "")
class Fee(models.Model):
fee_creator = models.ForeignKey(
Member,
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="Verantwortliche:r",
)
bankdata = models.ForeignKey(
BankData,
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name="Bankdaten",
)
job = models.TextField(verbose_name="Tätigkeit")
date_start = models.DateField(verbose_name="Start der Tätigkeit")
date_end = models.DateField(verbose_name="Ende der Tätigkeit")
amount = models.DecimalField(max_digits=7, decimal_places=2, verbose_name="Betrag (EUR)")
class Status(models.TextChoices):
SUBMITTED = "S", "Eingereicht"
APPROVED = "A", "Für Überweisung freigegeben"
PAYOUT = "P", "Ausbezahlt"
CLEARED = "C", "An Dekanat verrechnet"
status = models.CharField(
max_length=1,
choices=Status.choices,
default=Status.SUBMITTED,
verbose_name="Status",
)
date_created = models.DateTimeField(auto_now_add=True)
file_field = models.FileField(
upload_to="uploads/finance/fee/",
validators=[FileExtensionValidator(["pdf"])],
blank=True,
null=True,
verbose_name="Honorarnote",
)
comment = models.TextField(blank=True, default="", verbose_name="Kommentar")
class Meta:
verbose_name = "Honorar"
verbose_name_plural = "Honorare"
def __str__(self):
return f"Honorar #{self.id} / {self.job}"
def save(self, *args, **kwargs):
if not self.date_end:
self.date_end = self.date_start
super().save(*args, **kwargs)
@property
def filename(self):
return Path(self.file_field.name).name
class Resolution(models.Model): class Resolution(models.Model):
id = models.CharField(primary_key=True, max_length=128, verbose_name="Beschlussnummer") id = models.CharField(primary_key=True, max_length=128, verbose_name="Beschlussnummer")
name = models.CharField(max_length=128, verbose_name="Bezeichnung") name = models.CharField(max_length=128, verbose_name="Bezeichnung")

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@ from .views import (
BillCreateView, BillCreateView,
BillListView, BillListView,
BillUpdateView, BillUpdateView,
FeeCreateDoneView,
FeeCreateView,
FeeUpdateView,
ResolutionCreateView, ResolutionCreateView,
ResolutionDetailView, ResolutionDetailView,
ResolutionListView, ResolutionListView,
@@ -16,6 +19,7 @@ app_name = apps.FinanceConfig.name
urlpatterns = [ urlpatterns = [
path("", BillListView.as_view(), name="bill_list"), path("", BillListView.as_view(), name="bill_list"),
# Bill views
path("<int:pk>/", BillUpdateView.as_view(), name="bill_update"), path("<int:pk>/", BillUpdateView.as_view(), name="bill_update"),
path("create-bill/", BillCreateView.as_view(), name="bill_create"), path("create-bill/", BillCreateView.as_view(), name="bill_create"),
path( path(
@@ -23,6 +27,15 @@ urlpatterns = [
BillCreateDoneView.as_view(), BillCreateDoneView.as_view(),
name="bill_create_done", name="bill_create_done",
), ),
# Fee views
path("create-fee/", FeeCreateView.as_view(), name="fee_create"),
path(
"create-fee/<int:pk>/done/",
FeeCreateDoneView.as_view(),
name="fee_create_done",
),
path("fee/<int:pk>/", FeeUpdateView.as_view(), name="fee_update"),
# Resolution views
path("create-resolution/", ResolutionCreateView.as_view(), name="resolution_create"), path("create-resolution/", ResolutionCreateView.as_view(), name="resolution_create"),
path("resolutions/", ResolutionListView.as_view(), name="resolution_list"), path("resolutions/", ResolutionListView.as_view(), name="resolution_list"),
path( path(

View File

@@ -6,7 +6,7 @@ from django.core.files import File
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter
from pypdf.constants import FieldDictionaryAttributes as FA from pypdf.constants import FieldDictionaryAttributes as FA
from .models import Bill, Wiref from .models import Bill, Fee, Wiref
def generate_pdf(wiref): def generate_pdf(wiref):
@@ -80,3 +80,58 @@ def generate_pdf(wiref):
wiref.file_field.save(wiref_name, File(bytes_stream, wiref_name)) wiref.file_field.save(wiref_name, File(bytes_stream, wiref_name))
return True return True
def generate_fee_pdf(fee: Fee):
if not fee or fee.status != Fee.Status.SUBMITTED:
return False
# Get data for pdf
data = {}
data.update(
{
"Full_Name": fee.bankdata.name,
"Adresse": fee.bankdata.address,
# Change to the correct date format
"Date": str(fee.date_created.strftime("%d.%m.%Y")),
"Honorarnoten-Nummer": str(fee.pk),
"Taetigkeit_1": fee.job,
# Change to the correct date format
"Date_1": str(fee.date_start.strftime("%d.%m.%Y")),
# Change to the correct date format
"Date_2": str(fee.date_end.strftime("%d.%m.%Y")),
# Replace decimal separator from '.' to ','
"EUR_1": str(fee.amount).replace(".", ","),
"IBAN": fee.bankdata.iban,
"BIC": fee.bankdata.bic,
},
)
# Add mail only if a fet user create the fee
if fee.fee_creator:
mail = fee.fee_creator.mailaccount
data.update(
{
"Email": mail,
},
)
# Write data in pdf
pdf_path = os.path.join(os.path.dirname(__file__), "static/fee/Honorarnote-Vorlage.pdf")
reader = PdfReader(pdf_path)
writer = PdfWriter()
writer.append(reader)
writer.update_page_form_field_values(
writer.pages[0],
data,
)
with io.BytesIO() as bytes_stream:
writer.write(bytes_stream)
# Save pdf in fee
fee_name = f"Honorarnote-{fee.pk}.pdf"
fee.file_field.save(fee_name, File(bytes_stream, fee_name))
return True

View File

@@ -1,5 +1,5 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import Q from django.db.models import CharField, F, Q, Value
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
@@ -12,35 +12,42 @@ from posts.models import FetMeeting
from .forms import ( from .forms import (
BillCreateForm, BillCreateForm,
BillUpdateForm, BillUpdateForm,
FeeCreateForm,
FeeUpdateForm,
ResolutionCreateForm, ResolutionCreateForm,
ResolutionUpdateForm, ResolutionUpdateForm,
) )
from .models import BankData, Bill, Resolution from .models import BankData, Bill, Fee, Resolution
def set_bankdata(creator, name, iban, bic, saving): def set_bankdata(
if name != "" and iban != "" and bic != "": creator, name: str, iban: str, bic: str, saving: bool, address: str = ""
# Replace whitespaces in iban and bic text. ) -> BankData:
iban = iban.replace(" ", "") if not name or not iban or not bic:
bic = bic.replace(" ", "") return None
obj, created = BankData.objects.get_or_create( # Replace whitespaces in iban and bic text.
name=name, iban = iban.replace(" ", "")
iban=iban, bic = bic.replace(" ", "")
bic=bic,
defaults={"bankdata_creator": creator, "is_disabled": not saving}, obj, created = BankData.objects.get_or_create(
name=name,
iban=iban,
bic=bic,
defaults={"bankdata_creator": creator, "is_disabled": not saving, "address": address},
)
if not created and address:
BankData.objects.filter(id=obj.id).update(address=address)
if saving:
# Disable old bank data.
qs = BankData.objects.filter(
~Q(id=obj.id) & Q(bankdata_creator=obj.bankdata_creator) & Q(is_disabled=False)
) )
qs.update(is_disabled=True)
if saving is True: return obj
# Disable old bank data.
qs = BankData.objects.filter(
~Q(id=obj.id) & Q(bankdata_creator=obj.bankdata_creator) & Q(is_disabled=False),
)
qs.update(is_disabled=True)
return obj
return None
class BillCreateView(LoginRequiredMixin, CreateView): class BillCreateView(LoginRequiredMixin, CreateView):
@@ -81,9 +88,30 @@ class BillListView(LoginRequiredMixin, ListView):
paginate_by = 10 paginate_by = 10
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["fee_status"] = Fee.Status
context["bill_status"] = Bill.Status
return context
def get_queryset(self): def get_queryset(self):
qs = Bill.objects.filter(bill_creator__username=self.request.user) qs1 = (
return qs.order_by("-date") Fee.objects.filter(bankdata__bankdata_creator__username=self.request.user)
.values("amount", "status", "id")
.annotate(
date=F("date_start"), purpose=F("job"), model=Value("FEE", output_field=CharField())
)
)
qs2 = (
Bill.objects.filter(bill_creator__username=self.request.user)
.values("amount", "status", "id", "date", "purpose")
.annotate(model=Value("BILL", output_field=CharField()))
)
qs = qs1.union(qs2, all=True)
return qs.order_by("-date", "purpose")
class BillUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): class BillUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
@@ -120,6 +148,57 @@ class BillUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
return redirect("finance:bill_list") return redirect("finance:bill_list")
class FeeCreateView(LoginRequiredMixin, CreateView):
form_class = FeeCreateForm
model = Fee
template_name = "finance/fee/create.html"
def form_valid(self, form):
# Get or create bankdata object.
creator = form.cleaned_data["fee_creator"]
name = form.cleaned_data["name_text"]
iban = form.cleaned_data["iban_text"]
bic = form.cleaned_data["bic_text"]
address = form.cleaned_data["address_text"]
saving = form.cleaned_data["saving"]
form.instance.bankdata = set_bankdata(creator, name, iban, bic, saving, address)
add_log_action(self.request, form, "finance", "fee", True)
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# Request user for fee creator.
kwargs["user"] = self.request.user
return kwargs
def get_success_url(self):
return reverse("finance:fee_create_done", kwargs={"pk": self.object.pk})
class FeeCreateDoneView(LoginRequiredMixin, TemplateView):
template_name = "finance/fee/create_done.html"
class FeeUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
form_class = FeeUpdateForm
model = Fee
success_url = reverse_lazy("finance:bill_list")
template_name = "finance/fee/update.html"
def form_valid(self, form):
add_log_action(self.request, form, "finance", "fee", False)
return super().form_valid(form)
# Call fee if it's only yours.
def test_func(self):
return self.get_object().fee_creator.username == self.request.user.username
def handle_no_permission(self):
return redirect("finance:bill_list")
class ResolutionCreateView(LoginRequiredMixin, CreateView): class ResolutionCreateView(LoginRequiredMixin, CreateView):
form_class = ResolutionCreateForm form_class = ResolutionCreateForm
model = Resolution model = Resolution

View File

@@ -7,5 +7,6 @@
<a href="{% add_preserved_filters changelist_url %}" class="closelink">{% translate 'Close' %}</a> <a href="{% add_preserved_filters changelist_url %}" class="closelink">{% translate 'Close' %}</a>
{% endif %} {% endif %}
{% if generate_pdf %}<input type="submit" value="PDF File generieren" class="default" name="_generate_pdf">{% endif %} {% if generate_pdf %}<input type="submit" value="PDF File generieren" class="default" name="_generate_pdf">{% endif %}
{% if generate_fee_pdf %}<input type="submit" value="PDF File generieren" class="default" name="_generate_fee_pdf">{% endif %}
{{ block.super }} {{ block.super }}
{% endblock submit-row %} {% endblock submit-row %}

View File

@@ -0,0 +1,95 @@
{% extends 'base.html' %}
{% block title %}Neue Honorarnote einreichen{% endblock %}
{% block content %}
<!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1">
<h1 class="page-title">Neue Honorarnote einreichen</h1>
<form action="" enctype="multipart/form-data" method="POST" class="multiSectionForm max-w-2xl">
{% csrf_token %}
{% include "baseform/non_field_errors.html" %}
<section>
<h2>Kontodaten</h2>
<small>Angaben zu den Kontodaten für die Rückerstattung des Betrags.</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="sm:col-span-4">
{% include "baseform/text.html" with field=form.name_text %}
</div>
<div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.iban_text %}
</div>
<div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.bic_text %}
</div>
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.address_text %}
</div>
<div class="col-span-full">
{% include "baseform/checkbox.html" with field=form.saving %}
</div>
</div>
</section>
<section>
<h2>Tätigkeitsdetails</h2>
<small>Details zur erbrachten Tätigkeit angeben.</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.job %}
</div>
<div class="sm:col-span-3">
{% include "baseform/date.html" with field=form.date_start %}
</div>
<div class="sm:col-span-3">
{% include "baseform/date.html" with field=form.date_end %}
</div>
<div class="sm:col-span-2">
<label>
<span class="text-gray-700 dark:text-gray-200">{{ form.amount.label }}</span>
{% if form.amount.errors %}
<div class="alert alert-danger">
<div class="alert-body">{{ form.amount.errors }}</div>
</div>
{% endif %}
<input
type="number"
name="amount"
value={{ form.amount.value }}
{% if form.amount.field.required %}required{% endif %}
{% if form.amount.field.disabled %}disabled{% endif %}
min="0.00"
step="0.01"
placeholder="123,99"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-none shadow-sm focus:border-none focus:ring focus:ring-blue-200 dark:focus:ring-sky-700 focus:ring-opacity-50"
>
</label>
</div>
</div>
</section>
<section>
<h2>Kommentar</h2>
<small>Erfordert etwas zusätzlichen Erklärungsbedarf oder sollen nachträglich Informationen bearbeitet werden?</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.comment %}
</div>
</div>
</section>
<section>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="col-span-full">
{% include "baseform/checkbox.html" with field=form.conformation %}
</div>
</div>
</section>
<section class="flex justify-end">
<button type="submit" class="btn btn-primary w-full sm:w-auto" value="Einreichen">Honorar einreichen</button>
</section>
</form>
</main>
{% endblock content %}

View File

@@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block title %}Honorar wurde erfolgreich eingereicht{% endblock %}
{% block content %}
<!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1">
<section class="max-w-2xl mr-auto ml-auto text-center">
<p class="mt-6 text-gray-900 dark:text-gray-100">Honorar #{{ pk }} wurde erfolgreich eingereicht. Du hast deine Honorarnote nach Kontrolle durch die Finanzperson unter 'Meine Rechnungen / Honorarnoten' herunterladen.</p>
<div class="mt-10 flex items-center justify-center">
<a href="{% url 'home' %}" class="block btn btn-primary">Zur Startseite</a>
</div>
<div class="mt-10 flex items-center justify-center">
<a href="{% url 'home' %}" class="block btn btn-primary">Meine Rechnungen / Honorarnoten</a>
</div>
</section>
</main>
{% endblock content %}

View File

@@ -0,0 +1,114 @@
{% extends 'base.html' %}
{% block title %}Honorar {{ object.pk }}{% endblock %}
{% block content %}
<!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1">
<h1 class="page-title">Honorar {{ object.pk }}</h1>
<form action="" enctype="multipart/form-data" method="POST" class="multiSectionForm max-w-2xl">
{% csrf_token %}
{% include "baseform/non_field_errors.html" %}
<section>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="sm:col-span-3">
{% include "baseform/select.html" with field=form.status %}
</div>
</div>
</section>
<section>
<h2>Kontodaten</h2>
<small>Angaben zu den Kontodaten für die Rückerstattung des Betrags.</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="sm:col-span-4">
{% include "baseform/text.html" with field=form.name_text %}
</div>
<div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.iban_text %}
</div>
<div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.bic_text %}
</div>
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.address_text %}
</div>
<div class="col-span-full">
{% include "baseform/checkbox.html" with field=form.saving %}
</div>
</div>
</section>
<section>
<h2>Tätigkeitsdetails</h2>
<small>Details zur erbrachten Tätigkeit angeben.</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.job %}
</div>
<div class="sm:col-span-3">
{% include "baseform/date.html" with field=form.date_start %}
</div>
<div class="sm:col-span-3">
{% include "baseform/date.html" with field=form.date_end %}
</div>
<div class="sm:col-span-2">
<label>
<span class="text-gray-700 dark:text-gray-200">{{ form.amount.label }}</span>
{% if form.amount.errors %}
<div class="alert alert-danger">
<div class="alert-body">{{ form.amount.errors }}</div>
</div>
{% endif %}
<input
type="number"
name="amount"
value={{ form.amount.value }}
{% if form.amount.field.required %}required{% endif %}
{% if form.amount.field.disabled %}disabled{% endif %}
min="0.00"
step="0.01"
placeholder="123,99"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-none shadow-sm focus:border-none focus:ring focus:ring-blue-200 dark:focus:ring-sky-700 focus:ring-opacity-50"
>
</label>
</div>
</div>
</section>
<section>
<h2>Kommentar</h2>
<small>Erfordert etwas zusätzlichen Erklärungsbedarf oder sollen nachträglich Informationen bearbeitet werden?</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.comment %}
</div>
</div>
</section>
<section>
<h2>Honorarnote</h2>
<small>Hier kannst du die Honorarnote herunterladen.</small>
{% if object.file_field %}
<button type="button" class="btn btn-primary w-full sm:w-auto"><a href="{{ object.file_field.url }}">
<i class="fa-solid fa-file-pdf fa-fw text-red-800 dark:text-red-500 md:text-inherit group-hover:text-red-800 dark:group-hover:text-red-500"></i>
<span class="ml-2 sm:ml-1">{{ object.filename }}</span>
</a></button>
{% else %}
<span class="text-gray-700 dark:text-gray-200">Die Honorarnote ist aktuell noch in der Bearbeitung. Sie wird bald zum Download zur Verfügung stehen.</span>
{% endif %}
</section>
{% if form.status.value == object.Status.SUBMITTED %}
<section class="flex justify-end">
<button type="submit" class="btn btn-primary w-full sm:w-auto" value="Speichern">Änderung speichern</button>
</section>
{% else %}
<section class="flex justify-end">
<button type="button" class="btn btn-primary w-full sm:w-auto"><a href="{% url 'finance:bill_list' %}">Gehe zurück</a></button>
</section>
{% endif %}
</form>
</main>
{% endblock content %}

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Eingereichte Rechnungen{% endblock %} {% block title %}Meine Rechnungen / Honorarnoten{% endblock %}
{% block content %} {% block content %}
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1"> <main class="container mx-auto w-full px-4 my-8 flex-1">
<h1 class="page-title">Eingereichte Rechnungen</h1> <h1 class="page-title">Meine Rechnungen / Honorarnoten</h1>
<a href="{% url 'finance:bill_create' %}" class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto"> <a href="{% url 'finance:bill_create' %}" class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto">
<i class="fa-solid fa-plus mr-1"></i> Rechnung einreichen <i class="fa-solid fa-plus mr-1"></i> Rechnung einreichen
</a> </a>
@@ -16,7 +16,7 @@
<thead> <thead>
<tr> <tr>
<th class="text-right">Datum</th> <th class="text-right">Datum</th>
<th class="text-left">Verwendungszweck</th> <th class="text-left">Verwendungszweck / Tätigkeit</th>
<th class="text-right">Summe</th> <th class="text-right">Summe</th>
<th>Status</th> <th>Status</th>
<th></th> <th></th>
@@ -29,18 +29,34 @@
<td>{{ result.purpose }}</td> <td>{{ result.purpose }}</td>
<td class="text-right">{{ result.amount }}€</td> <td class="text-right">{{ result.amount }}€</td>
<td class="text-center"> <td class="text-center">
{% if result.status == result.Status.SUBMITTED %} {% if result.model == "BILL" %}
<span class="badge badge-info">{{ result.get_status_display }}</span> {% if result.status == bill_status.SUBMITTED %}
{% elif result.status == result.Status.INCOMPLETED %} <span class="badge badge-info">{{ bill_status.SUBMITTED.label }}</span>
<span class="badge badge-danger">{{ result.get_status_display }}</span> {% elif result.status == bill_status.INCOMPLETED %}
{% elif result.status == result.Status.CLEARED %} <span class="badge badge-danger">{{ bill_status.INCOMPLETED.label }}</span>
<span class="badge badge-warning">{{ result.get_status_display }}</span> {% elif result.status == bill_status.CLEARED %}
{% elif result.status == result.Status.FINISHED %} <span class="badge badge-warning">{{ bill_status.CLEARED.label }}</span>
<span class="badge badge-success">{{ result.get_status_display }}</span> {% elif result.status == bill_status.FINISHED %}
<span class="badge badge-success">{{ bill_status.FINISHED.label }}</span>
{% endif %}
{% elif result.model == "FEE" %}
{% if result.status == fee_status.SUBMITTED %}
<span class="badge badge-info">{{ fee_status.SUBMITTED.label }}</span>
{% elif result.status == fee_status.APPROVED %}
<span class="badge badge-warning">{{ fee_status.APPROVED.label }}</span>
{% elif result.status == fee_status.PAYOUT %}
<span class="badge badge-success">{{ fee_status.PAYOUT.label }}</span>
{% elif result.status == fee_status.CLEARED %}
<span class="badge badge-success">{{ fee_status.CLEARED.label }}</span>
{% endif %}
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="{% url 'finance:bill_update' result.id %}" class="btn btn-small btn-tertiary"><i class="fa-solid fa-pen-to-square" aria-label="Bearbeiten" title="Bearbeiten"></i></a> {% if result.model == "BILL" %}
<a href="{% url 'finance:bill_update' result.id %}" class="btn btn-small btn-tertiary"><i class="fa-solid fa-pen-to-square" aria-label="Bearbeiten" title="Bearbeiten"></i></a>
{% elif result.model == "FEE" %}
<a href="{% url 'finance:fee_update' result.id %}" class="btn btn-small btn-tertiary"><i class="fa-solid fa-pen-to-square" aria-label="Bearbeiten" title="Bearbeiten"></i></a>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -49,6 +49,12 @@
<span class="text-sm font-medium">Neue Rechnung einreichen</span> <span class="text-sm font-medium">Neue Rechnung einreichen</span>
</a> </a>
</li> </li>
<li>
<a href="{% url 'finance:fee_create' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white">
<i class="fa-solid fa-plus mr-2"></i>
<span class="text-sm font-medium">Neue Honorarnote einreichen</span>
</a>
</li>
<li> <li>
<a href="{% url 'finance:resolution_create' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white"> <a href="{% url 'finance:resolution_create' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white">
<i class="fa-solid fa-plus mr-2"></i> <i class="fa-solid fa-plus mr-2"></i>
@@ -58,7 +64,7 @@
<li> <li>
<a href="{% url 'finance:bill_list' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white"> <a href="{% url 'finance:bill_list' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white">
<i class="fa-solid fa-list mr-2"></i> <i class="fa-solid fa-list mr-2"></i>
<span class="text-sm font-medium">Eingereichte Rechnungen</span> <span class="text-sm font-medium">Meine Rechnungen / Honorarnoten</span>
</a> </a>
</li> </li>
<li> <li>