Compare commits

9 Commits

Author SHA1 Message Date
1f3fc92f6b add own bills to bankdata admin view 2025-02-07 14:45:41 +01:00
0ee85a2a77 fix import 2025-02-07 14:41:06 +01:00
6bbb92e9a9 add image extensions 2025-02-07 13:34:29 +01:00
23544cbaff fix type 2025-02-07 13:28:49 +01:00
58b74bdfab add fee 2025-02-07 13:26:47 +01:00
f6ffcd37da formatting 2025-02-04 00:42:25 +01:00
3fd408846e update django to 5.1.5 2025-02-04 00:41:21 +01:00
591b757a8e add python-dateutil 2025-02-04 00:40:35 +01:00
bc105eefcd Displayed only permanent and max 2 years old finance resolutions 2025-02-02 13:11:57 +01:00
17 changed files with 8646 additions and 54 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, Fee, Resolution, Wiref
from .utils import generate_pdf from .utils import generate_fee_pdf, generate_pdf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -153,10 +154,13 @@ class BankDataAdmin(admin.ModelAdmin):
form = BankDataAdminForm form = BankDataAdminForm
model = BankData model = BankData
inlines = (BillInline,)
list_display = [ list_display = [
"name", "name",
"iban", "iban",
"bic", "bic",
"address",
"is_disabled", "is_disabled",
] ]
@@ -375,6 +379,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

@@ -1,3 +1,7 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
import decimal
from django import forms from django import forms
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db.models import Count, Q from django.db.models import Count, Q
@@ -5,7 +9,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):
@@ -16,9 +20,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):
@@ -99,7 +103,7 @@ class BillCreateForm(forms.ModelForm):
"affiliation": "Abrechnungsbudget", "affiliation": "Abrechnungsbudget",
"payer": "Ursprüngliche Bezahlmethode", "payer": "Ursprüngliche Bezahlmethode",
"only_digital": "Ich habe nur eine digitale Rechnung.", "only_digital": "Ich habe nur eine digitale Rechnung.",
"file_field": "Rechnung hochladen (PDF)", "file_field": "Rechnung hochladen (PDF- und Bildformate erlaubt)",
"comment": "Kommentar", "comment": "Kommentar",
} }
@@ -190,7 +194,7 @@ class BillUpdateForm(forms.ModelForm):
"affiliation": "Abrechnungsbudget", "affiliation": "Abrechnungsbudget",
"payer": "Wie wurde die Rechnung bezahlt?", "payer": "Wie wurde die Rechnung bezahlt?",
"only_digital": "Ich habe nur eine digitale Rechnung.", "only_digital": "Ich habe nur eine digitale Rechnung.",
"file_field": "Neue Rechnung hochladen (PDF)", "file_field": "Neue Rechnung hochladen (PDF- und Bildformate erlaubt)",
"comment": "Kommentar", "comment": "Kommentar",
} }
@@ -280,6 +284,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
@@ -387,7 +556,7 @@ class BillAdminForm(forms.ModelForm):
"amount": "Betrag (EUR)", "amount": "Betrag (EUR)",
"comment": "Kommentar", "comment": "Kommentar",
"date": "Rechnungsdatum", "date": "Rechnungsdatum",
"file_field": "Rechnung hochladen (PDF)", "file_field": "Rechnung hochladen (PDF- und Bildformate erlaubt)",
"invoice": "Rechnungsaussteller", "invoice": "Rechnungsaussteller",
"only_digital": "Ich habe nur eine digitale Rechnung.", "only_digital": "Ich habe nur eine digitale Rechnung.",
"payer": "Wie wurde die Rechnung bezahlt?", "payer": "Wie wurde die Rechnung bezahlt?",
@@ -406,6 +575,15 @@ class BillAdminForm(forms.ModelForm):
self.fields["bill_creator"].widget.can_change_related = False self.fields["bill_creator"].widget.can_change_related = False
self.fields["bill_creator"].widget.can_delete_related = False self.fields["bill_creator"].widget.can_delete_related = False
# Displayed only permanent and max 2 years old finance resolutions.
self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter(
(
Q(option=Resolution.Option.FINANCE)
& Q(date__gt=datetime.now().date() - relativedelta(years=2))
)
| Q(option=Resolution.Option.PERMANENT)
)
# Delete wiref id from list if there are already 10 bills in wiref form. # Delete wiref id from list if there are already 10 bills in wiref form.
qs = ( qs = (
self.fields["wiref"].queryset.annotate(num_bills=Count("bill")).filter(num_bills__lt=10) self.fields["wiref"].queryset.annotate(num_bills=Count("bill")).filter(num_bills__lt=10)
@@ -427,6 +605,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()
@@ -439,9 +639,9 @@ class ResolutionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
budget = 0.0 budget = decimal.Decimal(0.0)
total = 0.0 total = decimal.Decimal(0.0)
if (resolution := kwargs.get("instance")) is not None: if resolution := kwargs.get("instance"):
for elem in Bill.objects.filter(resolution=resolution): for elem in Bill.objects.filter(resolution=resolution):
total += elem.amount total += elem.amount

View File

@@ -1,9 +1,13 @@
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
from members.models import Member from members.models import Member
from .validators import validate_bill_file_extension
class BankData(models.Model): class BankData(models.Model):
# members can be deleted but never their bank datas # members can be deleted but never their bank datas
@@ -17,6 +21,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 +40,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")
@@ -177,7 +248,7 @@ class Bill(models.Model):
file_field = models.FileField( file_field = models.FileField(
upload_to="uploads/finance/bills/", upload_to="uploads/finance/bills/",
validators=[FileExtensionValidator(["pdf"])], validators=[validate_bill_file_extension],
blank=True, blank=True,
null=True, null=True,
) )

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

@@ -0,0 +1,5 @@
from django.core.validators import FileExtensionValidator, get_available_image_extensions
def validate_bill_file_extension(value):
return FileExtensionValidator([*["pdf"], *get_available_image_extensions()])(value)

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

@@ -5,7 +5,7 @@ import sys
def main(): def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fet2020.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fet2020.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@@ -17,5 +17,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

Binary file not shown.

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>