add new feature finance (submit bill)
This commit is contained in:
@@ -59,6 +59,7 @@ INSTALLED_APPS = [
|
||||
"tasks.apps.TasksConfig",
|
||||
"gallery.apps.GalleryConfig",
|
||||
"intern.apps.InternConfig",
|
||||
"finance.apps.FinanceConfig",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ urlpatterns = [
|
||||
path("auth/", include("authentications.urls")),
|
||||
path("api/", include(router.urls)),
|
||||
path("ckeditor/", include("ckeditor_uploader.urls")),
|
||||
path("finance/", include("finance.urls")),
|
||||
path("gallery/", include("gallery.urls")),
|
||||
path("intern/", include("intern.urls")),
|
||||
path("jobs/", include("blackboard.urls")),
|
||||
|
||||
0
fet2020/finance/__init__.py
Normal file
0
fet2020/finance/__init__.py
Normal file
183
fet2020/finance/admin.py
Normal file
183
fet2020/finance/admin.py
Normal file
@@ -0,0 +1,183 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .forms import BankDataAdminForm, BillAdminForm, ResolutionAdminForm
|
||||
from .models import BankData, Bill, Resolution
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
|
||||
class BankDataAdmin(admin.ModelAdmin):
|
||||
form = BankDataAdminForm
|
||||
model = BankData
|
||||
|
||||
list_display = [
|
||||
"name",
|
||||
"iban",
|
||||
]
|
||||
|
||||
|
||||
class BillAdmin(admin.ModelAdmin):
|
||||
form = BillAdminForm
|
||||
model = Bill
|
||||
|
||||
list_display = [
|
||||
"id",
|
||||
"only_digital",
|
||||
"amount",
|
||||
"purpose",
|
||||
"resolution",
|
||||
"status",
|
||||
"bill_creator",
|
||||
"affiliation",
|
||||
"wiref_id",
|
||||
]
|
||||
|
||||
actions = ["make_cleared", "make_finished"]
|
||||
list_filter = ["status", "affiliation"]
|
||||
search_fields = ["wiref_id", "purpose"]
|
||||
ordering = ["date_created"]
|
||||
|
||||
readonly_fields = ["get_bankdata_name", "get_bankdata_iban", "get_bankdata_bic"]
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"bill_creator",
|
||||
"resolution",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Bankdaten",
|
||||
{
|
||||
"fields": (
|
||||
"payer",
|
||||
"bankdata",
|
||||
("get_bankdata_name", "get_bankdata_iban", "get_bankdata_bic"),
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Rechnung",
|
||||
{
|
||||
"fields": (
|
||||
"affiliation",
|
||||
"date",
|
||||
"invoice",
|
||||
"purpose",
|
||||
"amount",
|
||||
"only_digital",
|
||||
"file_field",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Sonstiges",
|
||||
{
|
||||
"fields": (
|
||||
"comment",
|
||||
"wiref_id",
|
||||
"status",
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def get_bankdata_name(self, obj):
|
||||
try:
|
||||
tmp = BankData.objects.get(id=obj.bankdata.id)
|
||||
except Exception:
|
||||
return "-"
|
||||
|
||||
return f"{ tmp.name }"
|
||||
|
||||
get_bankdata_name.short_description = "Kontoinhaber:in"
|
||||
|
||||
def get_bankdata_iban(self, obj):
|
||||
try:
|
||||
tmp = BankData.objects.get(id=obj.bankdata.id)
|
||||
except Exception:
|
||||
return "-"
|
||||
|
||||
return f"{ tmp.iban }"
|
||||
|
||||
get_bankdata_iban.short_description = "IBAN"
|
||||
|
||||
def get_bankdata_bic(self, obj):
|
||||
try:
|
||||
tmp = BankData.objects.get(id=obj.bankdata.id)
|
||||
except Exception:
|
||||
return "-"
|
||||
|
||||
return f"{ tmp.bic }"
|
||||
|
||||
get_bankdata_bic.short_description = "BIC"
|
||||
|
||||
@admin.action(description="Als 'Abgerechnet' markieren.")
|
||||
def make_cleared(self, request, queryset):
|
||||
updated = queryset.update(status="C")
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d Rechnung wurde als 'Abgerechnet' markiert.",
|
||||
"%d Rechnungen wurde als 'Abgerechnet' markiert.",
|
||||
updated,
|
||||
)
|
||||
% updated,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
@admin.action(description="Als 'Abgeschlossen' markieren.")
|
||||
def make_finished(self, request, queryset):
|
||||
updated = queryset.update(status="F")
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"%d Rechnung wurde als 'Abgeschlossen' markiert.",
|
||||
"%d Rechnungen wurde als 'Abgeschlossen' markiert.",
|
||||
updated,
|
||||
)
|
||||
% updated,
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
|
||||
class ResolutionAdmin(admin.ModelAdmin):
|
||||
form = ResolutionAdminForm
|
||||
model = Resolution
|
||||
|
||||
list_display = [
|
||||
"name",
|
||||
"id",
|
||||
"is_visible",
|
||||
]
|
||||
|
||||
|
||||
admin.site.register(BankData, BankDataAdmin)
|
||||
admin.site.register(Bill, BillAdmin)
|
||||
admin.site.register(Resolution, ResolutionAdmin)
|
||||
12
fet2020/finance/apps.py
Normal file
12
fet2020/finance/apps.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
from fet2020.utils import create_perms
|
||||
|
||||
|
||||
class FinanceConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "finance"
|
||||
|
||||
def ready(self):
|
||||
post_migrate.connect(create_perms, sender=self)
|
||||
264
fet2020/finance/forms.py
Normal file
264
fet2020/finance/forms.py
Normal file
@@ -0,0 +1,264 @@
|
||||
from django import forms
|
||||
from django.core.validators import ValidationError
|
||||
from django.forms import DateInput
|
||||
|
||||
from members.models import Member
|
||||
|
||||
from .models import BankData, Bill, Resolution
|
||||
|
||||
|
||||
class DateInput(DateInput):
|
||||
input_type = "date"
|
||||
|
||||
|
||||
class BankDataForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BankData
|
||||
|
||||
fields = ["iban", "bic", "name"]
|
||||
|
||||
labels = {"iban": "IBAN", "bic": "BIC", "name": "Kontoinhaber:in"}
|
||||
|
||||
|
||||
class BillCreateForm(forms.ModelForm):
|
||||
resolution_text = forms.CharField(max_length=128)
|
||||
name_text = forms.CharField(max_length=128)
|
||||
iban_text = forms.CharField(max_length=34)
|
||||
bic_text = forms.CharField(max_length=11)
|
||||
|
||||
class Meta:
|
||||
model = Bill
|
||||
|
||||
fields = [
|
||||
"bill_creator",
|
||||
"date",
|
||||
"invoice",
|
||||
"amount",
|
||||
"purpose",
|
||||
"affiliation",
|
||||
"payer",
|
||||
"only_digital",
|
||||
"file_field",
|
||||
"comment",
|
||||
"resolution",
|
||||
]
|
||||
|
||||
labels = {
|
||||
"bill_creator": "Verantwortliche:r für die Einreichung",
|
||||
"date": "Rechnungsdatum",
|
||||
"invoice": "Rechnungsaussteller",
|
||||
"amount": "Betrag (EUR)",
|
||||
"purpose": "Verwendungszweck",
|
||||
"affiliation": "Abrechnungsbudget",
|
||||
"payer": "Wie wurde die Rechnung bezahlt?",
|
||||
"only_digital": "Ich habe nur eine digitale Rechnung.",
|
||||
"file_field": "Rechnung hochladen (PDF)",
|
||||
"comment": "Kommentar",
|
||||
}
|
||||
|
||||
widgets = {
|
||||
"date": DateInput(format=("%Y-%m-%d")),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "user" in kwargs:
|
||||
user = kwargs.pop("user")
|
||||
else:
|
||||
user = None
|
||||
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["bill_creator"].initial = Member.objects.get(username=user.username)
|
||||
self.fields["bill_creator"].disabled = True
|
||||
|
||||
self.fields["invoice"].placeholder = "Firmenname\nStraße\nPLZ Ort"
|
||||
self.fields["invoice"].rows = 3
|
||||
|
||||
# bank data fields
|
||||
self.fields["name_text"].label = "Kontoinhaber:in"
|
||||
self.fields["name_text"].required = False
|
||||
|
||||
self.fields["iban_text"].label = "IBAN"
|
||||
self.fields["iban_text"].required = False
|
||||
|
||||
self.fields["bic_text"].label = "BIC"
|
||||
self.fields["bic_text"].required = False
|
||||
|
||||
# resolution fields
|
||||
self.fields["resolution_text"].label = "Beschlussnummer"
|
||||
self.fields["resolution_text"].required = False
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
amount = cleaned_data.get("amount")
|
||||
resolution = cleaned_data.get("resolution_text")
|
||||
|
||||
payer = cleaned_data.get("payer")
|
||||
name = cleaned_data.get("name_text")
|
||||
iban = cleaned_data.get("iban_text")
|
||||
bic = cleaned_data.get("bic_text")
|
||||
|
||||
only_digital = cleaned_data.get("only_digital")
|
||||
file_field = cleaned_data.get("file_field")
|
||||
|
||||
# check that amount is valid because invalid amount is a NoneType.
|
||||
if amount:
|
||||
if amount > 30 and resolution == "":
|
||||
raise ValidationError(
|
||||
"Die Beschlussnummer fehlt, weil der Betrag über 30€ beträgt "
|
||||
f"(Betrag: {amount}€)."
|
||||
)
|
||||
|
||||
if payer == "M":
|
||||
if name == "" or iban == "":
|
||||
raise ValidationError(
|
||||
f"Bankdaten unvollständig (Kontoinhaber: {name}, IBAN: {iban})."
|
||||
)
|
||||
|
||||
if payer == "V":
|
||||
cleaned_data["name_text"] = ""
|
||||
cleaned_data["iban_text"] = ""
|
||||
cleaned_data["bic_text"] = ""
|
||||
|
||||
if only_digital and file_field is None:
|
||||
raise ValidationError(f"Digitale Rechnung fehlt.")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class BillUpdateForm(forms.ModelForm):
|
||||
resolution_text = forms.CharField(max_length=128)
|
||||
name_text = forms.CharField(max_length=128)
|
||||
iban_text = forms.CharField(max_length=34)
|
||||
bic_text = forms.CharField(max_length=11)
|
||||
|
||||
class Meta:
|
||||
model = Bill
|
||||
|
||||
fields = [
|
||||
"bill_creator",
|
||||
"date",
|
||||
"invoice",
|
||||
"amount",
|
||||
"purpose",
|
||||
"affiliation",
|
||||
"payer",
|
||||
"only_digital",
|
||||
"file_field",
|
||||
"comment",
|
||||
"resolution",
|
||||
]
|
||||
|
||||
labels = {
|
||||
"bill_creator": "Verantwortliche:r für die Einreichung",
|
||||
"date": "Rechnungsdatum",
|
||||
"invoice": "Rechnungsaussteller",
|
||||
"amount": "Betrag (EUR)",
|
||||
"purpose": "Verwendungszweck",
|
||||
"affiliation": "Abrechnungsbudget",
|
||||
"payer": "Wie wurde die Rechnung bezahlt?",
|
||||
"only_digital": "Ich habe nur eine digitale Rechnung.",
|
||||
"file_field": "Rechnung hochladen (PDF)",
|
||||
"comment": "Kommentar",
|
||||
}
|
||||
|
||||
widgets = {
|
||||
"date": DateInput(format=("%Y-%m-%d")),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["bill_creator"].disabled = True
|
||||
self.fields["date"].disabled = True
|
||||
self.fields["invoice"].disabled = True
|
||||
self.fields["amount"].disabled = True
|
||||
self.fields["purpose"].disabled = True
|
||||
self.fields["affiliation"].disabled = True
|
||||
self.fields["payer"].disabled = True
|
||||
self.fields["only_digital"].disabled = True
|
||||
self.fields["file_field"].disabled = True
|
||||
self.fields["resolution"].disabled = True
|
||||
|
||||
# bank data fields
|
||||
if kwargs["instance"].bankdata:
|
||||
self.fields["name_text"].initial = kwargs["instance"].bankdata.name
|
||||
self.fields["iban_text"].initial = kwargs["instance"].bankdata.iban
|
||||
self.fields["bic_text"].initial = kwargs["instance"].bankdata.bic
|
||||
|
||||
self.fields["name_text"].disabled = True
|
||||
self.fields["name_text"].label = "Kontoinhaber:in"
|
||||
self.fields["name_text"].required = False
|
||||
|
||||
self.fields["iban_text"].disabled = True
|
||||
self.fields["iban_text"].label = "IBAN"
|
||||
self.fields["iban_text"].required = False
|
||||
|
||||
self.fields["bic_text"].disabled = True
|
||||
self.fields["bic_text"].label = "BIC"
|
||||
self.fields["bic_text"].required = False
|
||||
|
||||
# resolution fields
|
||||
if kwargs["instance"].resolution:
|
||||
self.fields["resolution_text"].initial = kwargs["instance"].resolution.name
|
||||
|
||||
self.fields["resolution_text"].disabled = True
|
||||
self.fields["resolution_text"].label = "Beschlussnummer"
|
||||
self.fields["resolution_text"].required = False
|
||||
|
||||
# comment disabled when bill is cleared or finished
|
||||
if kwargs["instance"].status != "S":
|
||||
self.fields["comment"].disabled = True
|
||||
|
||||
|
||||
class BankDataAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BankData
|
||||
|
||||
fields = "__all__"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["bankdata_creator"].widget.can_add_related = False
|
||||
self.fields["bankdata_creator"].widget.can_change_related = False
|
||||
self.fields["bankdata_creator"].widget.can_delete_related = False
|
||||
|
||||
|
||||
class BillAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bill
|
||||
|
||||
fields = "__all__"
|
||||
|
||||
labels = {
|
||||
"affiliation": "Abrechnungsbudget",
|
||||
"amount": "Betrag (EUR)",
|
||||
"comment": "Kommentar",
|
||||
"date": "Rechnungsdatum",
|
||||
"file_field": "Rechnung hochladen (PDF)",
|
||||
"invoice": "Rechnungsaussteller",
|
||||
"only_digital": "Ich habe nur eine digitale Rechnung.",
|
||||
"payer": "Wie wurde die Rechnung bezahlt?",
|
||||
"purpose": "Verwendungszweck",
|
||||
"resolution": "Beschlussnummer",
|
||||
}
|
||||
|
||||
widgets = {
|
||||
"date": DateInput(format=("%Y-%m-%d")),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["bill_creator"].widget.can_add_related = False
|
||||
self.fields["bill_creator"].widget.can_change_related = False
|
||||
self.fields["bill_creator"].widget.can_delete_related = False
|
||||
|
||||
|
||||
class ResolutionAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Resolution
|
||||
|
||||
fields = "__all__"
|
||||
178
fet2020/finance/migrations/0001_initial.py
Normal file
178
fet2020/finance/migrations/0001_initial.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# Generated by Django 4.2.2 on 2023-08-22 16:57
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("members", "0007_alter_member_username"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="BankData",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(max_length=128, verbose_name="Kontoinhaber:in"),
|
||||
),
|
||||
("iban", models.CharField(max_length=34, verbose_name="IBAN")),
|
||||
("bic", models.CharField(max_length=11, verbose_name="BIC")),
|
||||
(
|
||||
"is_disabled",
|
||||
models.BooleanField(default=False, verbose_name="deaktiviert"),
|
||||
),
|
||||
(
|
||||
"bankdata_creator",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="members.member",
|
||||
verbose_name="Verknüpfung zum Mitglied",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Bankdaten",
|
||||
"verbose_name_plural": "Bankdaten",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Resolution",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.CharField(max_length=128, primary_key=True, serialize=False),
|
||||
),
|
||||
("name", models.CharField(max_length=128)),
|
||||
(
|
||||
"is_visible",
|
||||
models.BooleanField(default=False, verbose_name="sichtbar"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Beschluss",
|
||||
"verbose_name_plural": "Beschlüsse",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Bill",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateField()),
|
||||
("invoice", models.TextField()),
|
||||
(
|
||||
"amount",
|
||||
models.DecimalField(
|
||||
decimal_places=2, max_digits=7, verbose_name="Betrag (EUR)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"purpose",
|
||||
models.CharField(max_length=140, verbose_name="Verwendungszweck"),
|
||||
),
|
||||
(
|
||||
"affiliation",
|
||||
models.CharField(
|
||||
choices=[("V", "Vereinsbudget"), ("B", "Offizielles Budget")],
|
||||
max_length=1,
|
||||
verbose_name="Abrechnungsbudget",
|
||||
),
|
||||
),
|
||||
(
|
||||
"payer",
|
||||
models.CharField(
|
||||
choices=[("M", "Privat"), ("V", "Verein (Safe/Kreditkarte)")],
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"only_digital",
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="Digitale Rechnung"
|
||||
),
|
||||
),
|
||||
(
|
||||
"file_field",
|
||||
models.FileField(
|
||||
blank=True,
|
||||
null=True,
|
||||
upload_to="uploads/finance/files/",
|
||||
validators=[
|
||||
django.core.validators.FileExtensionValidator(["pdf"])
|
||||
],
|
||||
),
|
||||
),
|
||||
("comment", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("S", "Eingereicht"),
|
||||
("C", "Abgerechnet"),
|
||||
("F", "Abgeschlossen"),
|
||||
],
|
||||
default="S",
|
||||
max_length=1,
|
||||
verbose_name="Status",
|
||||
),
|
||||
),
|
||||
("wiref_id", models.CharField(blank=True, max_length=10, null=True)),
|
||||
("date_created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"bankdata",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="finance.bankdata",
|
||||
verbose_name="Kontodaten",
|
||||
),
|
||||
),
|
||||
(
|
||||
"bill_creator",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="members.member",
|
||||
verbose_name="Verantwortliche:r",
|
||||
),
|
||||
),
|
||||
(
|
||||
"resolution",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="finance.resolution",
|
||||
verbose_name="Beschlussnummer",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Rechnung",
|
||||
"verbose_name_plural": "Rechnungen",
|
||||
},
|
||||
),
|
||||
]
|
||||
0
fet2020/finance/migrations/__init__.py
Normal file
0
fet2020/finance/migrations/__init__.py
Normal file
121
fet2020/finance/models.py
Normal file
121
fet2020/finance/models.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from django.core.validators import FileExtensionValidator, ValidationError
|
||||
from django.db import models
|
||||
|
||||
from members.models import Member
|
||||
|
||||
|
||||
class BankData(models.Model):
|
||||
# members can be deleted but never their bank datas
|
||||
bankdata_creator = models.ForeignKey(
|
||||
Member,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
verbose_name="Verknüpfung zum Mitglied",
|
||||
)
|
||||
name = models.CharField(max_length=128, verbose_name="Kontoinhaber:in")
|
||||
iban = models.CharField(max_length=34, verbose_name="IBAN")
|
||||
bic = models.CharField(max_length=11, verbose_name="BIC")
|
||||
|
||||
is_disabled = models.BooleanField(default=False, verbose_name="deaktiviert")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Bankdaten"
|
||||
verbose_name_plural = "Bankdaten"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - {self.iban}"
|
||||
|
||||
|
||||
class Resolution(models.Model):
|
||||
id = models.CharField(primary_key=True, max_length=128)
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
is_visible = models.BooleanField(default=False, verbose_name="sichtbar")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Beschluss"
|
||||
verbose_name_plural = "Beschlüsse"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class Bill(models.Model):
|
||||
# members can be deleted but never their bills
|
||||
bill_creator = models.ForeignKey(
|
||||
Member, on_delete=models.PROTECT, verbose_name="Verantwortliche:r"
|
||||
)
|
||||
|
||||
bankdata = models.ForeignKey(
|
||||
BankData,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Kontodaten",
|
||||
)
|
||||
|
||||
resolution = models.ForeignKey(
|
||||
Resolution,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Beschlussnummer",
|
||||
)
|
||||
|
||||
date = models.DateField()
|
||||
invoice = models.TextField()
|
||||
amount = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, verbose_name="Betrag (EUR)"
|
||||
)
|
||||
purpose = models.CharField(max_length=140, verbose_name="Verwendungszweck")
|
||||
|
||||
class Affiliation(models.TextChoices):
|
||||
VEREIN = "V", "Vereinsbudget"
|
||||
OFFICIAL = "B", "Offizielles Budget"
|
||||
|
||||
affiliation = models.CharField(
|
||||
max_length=1, choices=Affiliation.choices, verbose_name="Abrechnungsbudget"
|
||||
)
|
||||
|
||||
class Payer(models.TextChoices):
|
||||
ME = "M", "Privat"
|
||||
VEREIN = "V", "Verein (Safe/Kreditkarte)"
|
||||
|
||||
payer = models.CharField(max_length=1, choices=Payer.choices)
|
||||
|
||||
only_digital = models.BooleanField(default=False, verbose_name="Digitale Rechnung")
|
||||
|
||||
file_field = models.FileField(
|
||||
upload_to="uploads/finance/files/",
|
||||
validators=[FileExtensionValidator(["pdf"])],
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
comment = models.TextField(blank=True, null=True)
|
||||
|
||||
class Status(models.TextChoices):
|
||||
SUBMITTED = "S", "Eingereicht"
|
||||
CLEARED = "C", "Abgerechnet"
|
||||
FINISHED = "F", "Abgeschlossen"
|
||||
|
||||
status = models.CharField(
|
||||
max_length=1,
|
||||
choices=Status.choices,
|
||||
default=Status.SUBMITTED,
|
||||
verbose_name="Status",
|
||||
)
|
||||
wiref_id = models.CharField(max_length=10, blank=True, null=True)
|
||||
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Rechnung"
|
||||
verbose_name_plural = "Rechnungen"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.purpose}"
|
||||
|
||||
def clean(self):
|
||||
if self.status is None:
|
||||
self.status = Bill.Status.SUBMITTED
|
||||
0
fet2020/finance/tests.py
Normal file
0
fet2020/finance/tests.py
Normal file
16
fet2020/finance/urls.py
Normal file
16
fet2020/finance/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import apps, views
|
||||
from .views import (
|
||||
BillCreateView,
|
||||
BillListView,
|
||||
BillUpdateView,
|
||||
)
|
||||
|
||||
app_name = apps.FinanceConfig.name
|
||||
|
||||
urlpatterns = [
|
||||
path("", BillListView.as_view(), name="bill_list"),
|
||||
path("<int:pk>/", BillUpdateView.as_view(), name="bill_update"),
|
||||
path("create-bill/", BillCreateView.as_view(), name="bill_create"),
|
||||
]
|
||||
80
fet2020/finance/views.py
Normal file
80
fet2020/finance/views.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
|
||||
from fet2020.utils import add_log_action, create_formsets
|
||||
from members.models import Member
|
||||
|
||||
from .forms import BankDataForm, BillCreateForm, BillUpdateForm
|
||||
from .models import Bill, BankData, Resolution
|
||||
|
||||
|
||||
class BillCreateView(LoginRequiredMixin, CreateView):
|
||||
form_class = BillCreateForm
|
||||
model = Bill
|
||||
success_url = reverse_lazy("finance:bill_list")
|
||||
template_name = "finance/bill_create.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
# get or create resolution object
|
||||
resolution = form.cleaned_data["resolution_text"]
|
||||
if resolution != "":
|
||||
obj, created = Resolution.objects.get_or_create(
|
||||
id=resolution, defaults={"name": resolution}
|
||||
)
|
||||
form.instance.resolution = obj
|
||||
|
||||
# get or create bankdata object
|
||||
name = form.cleaned_data["name_text"]
|
||||
iban = form.cleaned_data["iban_text"]
|
||||
bic = form.cleaned_data["bic_text"]
|
||||
if name != "" and iban != "" and bic != "":
|
||||
obj, created = BankData.objects.get_or_create(
|
||||
name=name,
|
||||
iban=iban,
|
||||
bic=bic,
|
||||
defaults={"bankdata_creator": form.cleaned_data["bill_creator"]},
|
||||
)
|
||||
form.instance.bankdata = obj
|
||||
|
||||
add_log_action(self.request, form, "finance", "bill", True)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["user"] = self.request.user
|
||||
return kwargs
|
||||
|
||||
|
||||
class BillListView(LoginRequiredMixin, ListView):
|
||||
model = Bill
|
||||
template_name = "finance/index.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return Bill.objects.filter(bill_creator__username=self.request.user)
|
||||
|
||||
|
||||
class BillUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
||||
form_class = BillUpdateForm
|
||||
model = Bill
|
||||
success_url = reverse_lazy("finance:bill_list")
|
||||
template_name = "finance/bill_update.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
add_log_action(self.request, form, "finance", "bill", False)
|
||||
return super().form_valid(form)
|
||||
|
||||
# call bill if it's only yours
|
||||
def test_func(self):
|
||||
if self.get_object().bill_creator.username == self.request.user.username:
|
||||
return True
|
||||
|
||||
# call handle_no_permissions method
|
||||
return False
|
||||
|
||||
def handle_no_permission(self):
|
||||
return redirect("finance:bill_list")
|
||||
24
fet2020/templates/baseform/text_with_suggestions.html
Normal file
24
fet2020/templates/baseform/text_with_suggestions.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<label class="block">
|
||||
<span class="text-gray-700 dark:text-gray-200">{{ field.label }}</span>
|
||||
{% if field.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<div class="alert-body">{{ field.errors }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-y-2 md:gap-y-0 md:gap-x-2">
|
||||
<input
|
||||
type="text"
|
||||
name="{{ field.name }}"
|
||||
{% if field.value %}value="{{ field.value }}"{% endif %}
|
||||
{% if field.field.required %}required{% endif %}
|
||||
{% if field.field.disabled %}disabled{% endif %}
|
||||
class="mt-1 block w-full disabled:bg-gray-200 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"
|
||||
>
|
||||
<a href="" class="block btn btn-primary md:flex-grow lg:flex-grow-0"><i class="fa-solid fa-plus-square my-2"></i></a>
|
||||
</div>
|
||||
|
||||
{% if field.help_text %}
|
||||
<span class="text-gray-700 dark:text-gray-200">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
67
fet2020/templates/finance/bill_create.html
Normal file
67
fet2020/templates/finance/bill_create.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Neue Rechnung einreichen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Neue Rechnung einreichen</h1>
|
||||
<div class="w-full h-full flex-1 flex justify-center items-center">
|
||||
<form action="" enctype="multipart/form-data" method="POST" class="w-full max-w-xs sm:max-w-prose sm:px-28 sm:py-4 grid grid-cols-1 gap-y-3 sm:gap-y-6 text-gray-900">
|
||||
{% csrf_token %}
|
||||
{% include "baseform/non_field_errors.html" %}
|
||||
|
||||
{% include "baseform/select.html" with field=form.bill_creator %}
|
||||
|
||||
<hr>
|
||||
<span class="text-gray-700 dark:text-gray-200">Bankdaten</span>
|
||||
|
||||
{% include "baseform/select.html" with field=form.payer %}
|
||||
{% include "baseform/text_with_suggestions.html" with field=form.name_text %}
|
||||
{% include "baseform/text.html" with field=form.iban_text %}
|
||||
{% include "baseform/text.html" with field=form.bic_text %}
|
||||
|
||||
<hr>
|
||||
<span class="text-gray-700 dark:text-gray-200">Rechnung</span>
|
||||
|
||||
{% include "baseform/date.html" with field=form.date %}
|
||||
{% include "baseform/textarea.html" with field=form.invoice %}
|
||||
{% include "baseform/text.html" with field=form.purpose %}
|
||||
|
||||
<label class="block">
|
||||
<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"
|
||||
value={{ form.amount.value }}
|
||||
min="0.00"
|
||||
step="0.01"
|
||||
name="amount"
|
||||
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"
|
||||
required
|
||||
>
|
||||
</label>
|
||||
|
||||
{% include "baseform/checkbox.html" with field=form.only_digital %}
|
||||
{% include "baseform/file.html" with field=form.file_field %}
|
||||
|
||||
<hr>
|
||||
<span class="text-gray-700 dark:text-gray-200">Beschlusslage</span>
|
||||
|
||||
{% include "baseform/select.html" with field=form.affiliation %}
|
||||
{% include "baseform/text.html" with field=form.resolution_text %}
|
||||
|
||||
<hr>
|
||||
{% include "baseform/textarea.html" with field=form.comment %}
|
||||
|
||||
<div class="flex flex-col-reverse sm:flex-row gap-3 justify-end pt-4 sm:pt-0">
|
||||
<input type="submit" class="block btn btn-primary" value="Einreichen">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
70
fet2020/templates/finance/bill_update.html
Normal file
70
fet2020/templates/finance/bill_update.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Rechnung {{ object.pk }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Rechnung {{ object.pk }}</h1>
|
||||
<div class="w-full h-full flex-1 flex justify-center items-center">
|
||||
<form action="" enctype="multipart/form-data" method="POST" class="w-full max-w-xs sm:max-w-prose sm:px-28 sm:py-4 grid grid-cols-1 gap-y-3 sm:gap-y-6 text-gray-900">
|
||||
{% csrf_token %}
|
||||
{% include "baseform/non_field_errors.html" %}
|
||||
|
||||
{% include "baseform/select.html" with field=form.bill_creator %}
|
||||
|
||||
<hr>
|
||||
<span class="text-gray-700 dark:text-gray-200">Bankdaten</span>
|
||||
|
||||
{% include "baseform/select.html" with field=form.payer %}
|
||||
{% if form.name_text.value %}
|
||||
{% include "baseform/text.html" with field=form.name_text %}
|
||||
{% endif %}
|
||||
{% if form.iban_text.value %}
|
||||
{% include "baseform/text.html" with field=form.iban_text %}
|
||||
{% endif %}
|
||||
{% if form.bic_text.value %}
|
||||
{% include "baseform/text.html" with field=form.bic_text %}
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<span class="text-gray-700 dark:text-gray-200">Rechnung</span>
|
||||
|
||||
{% include "baseform/text.html" with field=form.date %}
|
||||
{% include "baseform/textarea.html" with field=form.invoice %}
|
||||
{% include "baseform/text.html" with field=form.purpose %}
|
||||
{% include "baseform/text.html" with field=form.amount %}
|
||||
|
||||
{% if form.file_field.value %}
|
||||
{% include "baseform/checkbox.html" with field=form.only_digital %}
|
||||
|
||||
<label>
|
||||
<span class="text-gray-700 dark:text-gray-200">Derzeit:</span>
|
||||
|
||||
<a
|
||||
href="{{ form.file_field.value.url }}"
|
||||
class="text-gray-700 dark:text-gray-200 block w-full mt-1 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"
|
||||
>{{ form.file_field.value }}</a>
|
||||
</label>
|
||||
{% else %}
|
||||
<span class="text-gray-700 dark:text-gray-200">Keine digitale Rechnung eingereicht.</span>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<span class="text-gray-700 dark:text-gray-200">Beschlusslage</span>
|
||||
|
||||
{% include "baseform/select.html" with field=form.affiliation %}
|
||||
{% if form.resolution_text.value %}
|
||||
{% include "baseform/text.html" with field=form.resolution_text %}
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
{% include "baseform/textarea.html" with field=form.comment %}
|
||||
|
||||
<div class="flex flex-col-reverse sm:flex-row gap-3 justify-end pt-4 sm:pt-0">
|
||||
<input type="submit" class="block btn btn-primary" value="Speichern">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
50
fet2020/templates/finance/index.html
Normal file
50
fet2020/templates/finance/index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Übersicht über die Rechnungen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Übersicht über alle von dir eingereichten Rechnungen</h1>
|
||||
|
||||
<section>
|
||||
<div class="mx-auto max-w-prose flex flex-col gap-4">
|
||||
|
||||
{% for result in object_list %}
|
||||
<a class="flex gap-x-4 group" href="{% url 'finance:bill_update' result.id %}">
|
||||
<article class="flex-grow-0 self-center">
|
||||
<h2 class="line-clamp-1 hover:underline decoration-1 text-gray-800 dark:text-gray-200 font-medium">Verwendungszweck: {{ result.purpose }}</h2>
|
||||
<ul class="text-gray-700 dark:text-gray-300 text-sm sm:text-base">
|
||||
<li><i class="fa-fw text-gray-600 dark:text-gray-400 mr-1"></i>{{ result.date }}</li>
|
||||
<li><i class="fa-fw text-gray-600 dark:text-gray-400 mr-1"></i>{{ result.amount }}€</li>
|
||||
<li><i class="fa-fw text-gray-600 dark:text-gray-400 mr-1"></i>Status: {{ result.get_status_display }}</li>
|
||||
</ul>
|
||||
</article>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if not object_list %}
|
||||
<section>
|
||||
<div class="mx-auto max-w-prose flex flex-col gap-4">
|
||||
<h2 class="mb-1 text-gray-700 dark:text-gray-200">Keine Rechnungen für dich in dieser Liste.</h2>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-col gap-y-4 max-w-prose mx-auto text-gray-700 dark:text-gray-300">
|
||||
<section>
|
||||
<div class="flex flex-col md:flex-row gap-y-2 md:gap-y-0 md:gap-x-2 lg:justify-end mt-4">
|
||||
<a
|
||||
href="{% url 'finance:bill_create' %}"
|
||||
class="btn btn-primary block md:flex-grow lg:flex-grow-0"
|
||||
>
|
||||
<i class="fa-solid fa-plus-square mr-2"></i>Rechnung einreichen
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -42,6 +42,18 @@
|
||||
<span class="text-sm font-medium">Neue Fachschaftssitzung</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'finance:bill_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 Rechnung einreichen</span>
|
||||
</a>
|
||||
</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">
|
||||
<i class="fa-solid fa-list mr-2"></i>
|
||||
<span class="text-sm font-medium">Übersicht über alle von dir eingereichten Rechnungen</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" data-dial-toggle="speed-dial-menu-dropdown" aria-controls="speed-dial-menu-dropdown" aria-expanded="false" class="flex justify-center items-center ml-auto w-14 h-14 text-white bg-blue-700 rounded-full hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 focus:outline-none dark:focus:ring-blue-800">
|
||||
|
||||
Reference in New Issue
Block a user