add fee
This commit is contained in:
@@ -78,8 +78,9 @@ def get_app_list(self, request, app_label=None):
|
||||
ordering = {
|
||||
"Rechnungen": 1,
|
||||
"Wiref Formulare": 2,
|
||||
"Beschlüsse": 3,
|
||||
"Bankdaten": 4,
|
||||
"Honorare": 3,
|
||||
"Beschlüsse": 4,
|
||||
"Bankdaten": 5,
|
||||
}
|
||||
app["models"].sort(key=lambda x: ordering[x["name"]])
|
||||
|
||||
|
||||
@@ -15,11 +15,12 @@ from .forms import (
|
||||
BankDataAdminForm,
|
||||
BillAdminForm,
|
||||
BillInlineForm,
|
||||
FeeAdminForm,
|
||||
ResolutionAdminForm,
|
||||
WirefAdminForm,
|
||||
)
|
||||
from .models import BankData, Bill, Resolution, Wiref
|
||||
from .utils import generate_pdf
|
||||
from .models import BankData, Bill, Budget, Fee, Resolution, Wiref
|
||||
from .utils import generate_fee_pdf, generate_pdf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -157,6 +158,7 @@ class BankDataAdmin(admin.ModelAdmin):
|
||||
"name",
|
||||
"iban",
|
||||
"bic",
|
||||
"address",
|
||||
"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)
|
||||
class ResolutionAdmin(admin.ModelAdmin):
|
||||
form = ResolutionAdminForm
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.forms import DateInput
|
||||
|
||||
from members.models import Member
|
||||
|
||||
from .models import BankData, Bill, Resolution, Wiref
|
||||
from .models import BankData, Bill, Fee, Resolution, Wiref
|
||||
|
||||
|
||||
class DateInput(DateInput):
|
||||
@@ -19,9 +19,9 @@ class BankDataForm(forms.ModelForm):
|
||||
class Meta:
|
||||
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):
|
||||
@@ -283,6 +283,171 @@ class BillUpdateForm(forms.ModelForm):
|
||||
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 Meta:
|
||||
model = Resolution
|
||||
@@ -439,6 +604,28 @@ class BillAdminForm(forms.ModelForm):
|
||||
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):
|
||||
total = forms.CharField()
|
||||
budget_remaining = forms.CharField()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.validators import FileExtensionValidator, ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@@ -17,6 +19,8 @@ class BankData(models.Model):
|
||||
iban = models.CharField(max_length=34, verbose_name="IBAN")
|
||||
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")
|
||||
|
||||
class Meta:
|
||||
@@ -34,6 +38,71 @@ class BankData(models.Model):
|
||||
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):
|
||||
id = models.CharField(primary_key=True, max_length=128, verbose_name="Beschlussnummer")
|
||||
name = models.CharField(max_length=128, verbose_name="Bezeichnung")
|
||||
|
||||
7768
fet2020/finance/static/fee/Honorarnote-Vorlage.pdf
Normal file
7768
fet2020/finance/static/fee/Honorarnote-Vorlage.pdf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,9 @@ from .views import (
|
||||
BillCreateView,
|
||||
BillListView,
|
||||
BillUpdateView,
|
||||
FeeCreateDoneView,
|
||||
FeeCreateView,
|
||||
FeeUpdateView,
|
||||
ResolutionCreateView,
|
||||
ResolutionDetailView,
|
||||
ResolutionListView,
|
||||
@@ -16,6 +19,7 @@ app_name = apps.FinanceConfig.name
|
||||
|
||||
urlpatterns = [
|
||||
path("", BillListView.as_view(), name="bill_list"),
|
||||
# Bill views
|
||||
path("<int:pk>/", BillUpdateView.as_view(), name="bill_update"),
|
||||
path("create-bill/", BillCreateView.as_view(), name="bill_create"),
|
||||
path(
|
||||
@@ -23,6 +27,15 @@ urlpatterns = [
|
||||
BillCreateDoneView.as_view(),
|
||||
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("resolutions/", ResolutionListView.as_view(), name="resolution_list"),
|
||||
path(
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.core.files import File
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf.constants import FieldDictionaryAttributes as FA
|
||||
|
||||
from .models import Bill, Wiref
|
||||
from .models import Bill, Fee, Wiref
|
||||
|
||||
|
||||
def generate_pdf(wiref):
|
||||
@@ -80,3 +80,58 @@ def generate_pdf(wiref):
|
||||
wiref.file_field.save(wiref_name, File(bytes_stream, wiref_name))
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.urls import reverse, reverse_lazy
|
||||
from django.views.generic import ListView, TemplateView
|
||||
@@ -12,35 +12,42 @@ from posts.models import FetMeeting
|
||||
from .forms import (
|
||||
BillCreateForm,
|
||||
BillUpdateForm,
|
||||
FeeCreateForm,
|
||||
FeeUpdateForm,
|
||||
ResolutionCreateForm,
|
||||
ResolutionUpdateForm,
|
||||
)
|
||||
from .models import BankData, Bill, Resolution
|
||||
from .models import BankData, Bill, Fee, Resolution
|
||||
|
||||
|
||||
def set_bankdata(creator, name, iban, bic, saving):
|
||||
if name != "" and iban != "" and bic != "":
|
||||
# Replace whitespaces in iban and bic text.
|
||||
iban = iban.replace(" ", "")
|
||||
bic = bic.replace(" ", "")
|
||||
def set_bankdata(
|
||||
creator, name: str, iban: str, bic: str, saving: bool, address: str = ""
|
||||
) -> BankData:
|
||||
if not name or not iban or not bic:
|
||||
return None
|
||||
|
||||
obj, created = BankData.objects.get_or_create(
|
||||
name=name,
|
||||
iban=iban,
|
||||
bic=bic,
|
||||
defaults={"bankdata_creator": creator, "is_disabled": not saving},
|
||||
# Replace whitespaces in iban and bic text.
|
||||
iban = iban.replace(" ", "")
|
||||
bic = bic.replace(" ", "")
|
||||
|
||||
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:
|
||||
# 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
|
||||
return obj
|
||||
|
||||
|
||||
class BillCreateView(LoginRequiredMixin, CreateView):
|
||||
@@ -81,9 +88,30 @@ class BillListView(LoginRequiredMixin, ListView):
|
||||
|
||||
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):
|
||||
qs = Bill.objects.filter(bill_creator__username=self.request.user)
|
||||
return qs.order_by("-date")
|
||||
qs1 = (
|
||||
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):
|
||||
@@ -120,6 +148,57 @@ class BillUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
||||
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):
|
||||
form_class = ResolutionCreateForm
|
||||
model = Resolution
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
<a href="{% add_preserved_filters changelist_url %}" class="closelink">{% translate 'Close' %}</a>
|
||||
{% 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 }}
|
||||
{% endblock submit-row %}
|
||||
|
||||
95
fet2020/templates/finance/fee/create.html
Normal file
95
fet2020/templates/finance/fee/create.html
Normal 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 %}
|
||||
18
fet2020/templates/finance/fee/create_done.html
Normal file
18
fet2020/templates/finance/fee/create_done.html
Normal 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 %}
|
||||
114
fet2020/templates/finance/fee/update.html
Normal file
114
fet2020/templates/finance/fee/update.html
Normal 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 %}
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Eingereichte Rechnungen{% endblock %}
|
||||
{% block title %}Meine Rechnungen / Honorarnoten{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<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">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Rechnung einreichen
|
||||
</a>
|
||||
@@ -16,7 +16,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<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>Status</th>
|
||||
<th></th>
|
||||
@@ -29,18 +29,34 @@
|
||||
<td>{{ result.purpose }}</td>
|
||||
<td class="text-right">{{ result.amount }}€</td>
|
||||
<td class="text-center">
|
||||
{% if result.status == result.Status.SUBMITTED %}
|
||||
<span class="badge badge-info">{{ result.get_status_display }}</span>
|
||||
{% elif result.status == result.Status.INCOMPLETED %}
|
||||
<span class="badge badge-danger">{{ result.get_status_display }}</span>
|
||||
{% elif result.status == result.Status.CLEARED %}
|
||||
<span class="badge badge-warning">{{ result.get_status_display }}</span>
|
||||
{% elif result.status == result.Status.FINISHED %}
|
||||
<span class="badge badge-success">{{ result.get_status_display }}</span>
|
||||
{% if result.model == "BILL" %}
|
||||
{% if result.status == bill_status.SUBMITTED %}
|
||||
<span class="badge badge-info">{{ bill_status.SUBMITTED.label }}</span>
|
||||
{% elif result.status == bill_status.INCOMPLETED %}
|
||||
<span class="badge badge-danger">{{ bill_status.INCOMPLETED.label }}</span>
|
||||
{% elif result.status == bill_status.CLEARED %}
|
||||
<span class="badge badge-warning">{{ bill_status.CLEARED.label }}</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 %}
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
<span class="text-sm font-medium">Neue Rechnung einreichen</span>
|
||||
</a>
|
||||
</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>
|
||||
<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>
|
||||
@@ -58,7 +64,7 @@
|
||||
<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">
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
Reference in New Issue
Block a user