19 Commits

Author SHA1 Message Date
28d3d99754 update account holder in wiref file 2025-11-17 12:17:46 +01:00
78c2860cca update etherpad to 2.5.2; fix charset and add auth method 2025-11-04 22:02:07 +01:00
2024466a48 Fix the help text 2025-11-04 18:49:03 +01:00
72570e25c2 add firstname and surname to search field 2025-11-03 18:46:24 +01:00
a3b252c9be Fix: Find the current last day; Use the correct get now time function 2025-10-31 13:30:43 +01:00
b0e686245a Merge branch 'master' of https://git.fet.at/bofh/fet2020 2025-10-31 13:04:28 +01:00
sebivh
b50c010b3b Add persistant Storage to Container Databases 2025-10-30 23:21:25 +01:00
be581675cd update homepage version to 2.2.1 2025-10-30 15:42:48 +01:00
c1519bab0f add migrations 2025-10-30 15:40:49 +01:00
585bc60676 update assets 2025-10-30 15:39:58 +01:00
b9943b7d41 add help text for status 2025-10-30 15:20:14 +01:00
65ac5ae18e fix: sending mail; add email host user and pwd for authentication 2025-10-30 15:05:36 +01:00
8ff3905657 Add total deposit calculation to Rental model 2025-10-30 15:00:20 +01:00
5d2a052c1e add internal rentals with zero deposit 2025-10-30 14:05:53 +01:00
4a7076b120 fix: third line in products in pdf file 2025-10-30 13:59:00 +01:00
2204c07deb fix: path to pdf file 2025-10-30 13:58:11 +01:00
07449db128 add migrations 2025-10-30 13:06:28 +01:00
5d9ad679de Fix: auto-split rentals with >5 items; update create_done text 2025-10-30 12:59:11 +01:00
0e1a61cefc sorted all rentalitems alphabetically 2025-10-30 12:45:01 +01:00
26 changed files with 969 additions and 824 deletions

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ flowbite
gallery/* gallery/*
tailwind tailwind
whoosh_index whoosh_index
databases/django

View File

@@ -96,6 +96,8 @@ docker build -t django-nginx-image -f nginx/Dockerfile ./nginx
### Start docker container ### Start docker container
Add email password for 'Verleih' account to EMAIL_HOST_PASSWORD in the docker compose file!
Build the docker containers: Build the docker containers:
```bash ```bash
@@ -134,6 +136,10 @@ ckeditor -> django-prose-editor
## Version History ## Version History
2.2.1
* Fix rental (view, pdf file, sending mail)
2.2.0 2.2.0
* Add rental * Add rental

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,7 @@ services:
depends_on: depends_on:
- django-homepage - django-homepage
volumes: volumes:
- files-volume:/usr/src/app/files - ./files:/usr/src/app/files
- ./gallery:/usr/src/app/files/uploads/gallery - ./gallery:/usr/src/app/files/uploads/gallery
- ./assets:/usr/src/app/assets:ro - ./assets:/usr/src/app/assets:ro
networks: networks:
@@ -25,6 +25,8 @@ services:
MYSQL_USER: "user" MYSQL_USER: "user"
MYSQL_PASSWORD: "hgu" MYSQL_PASSWORD: "hgu"
ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3" ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3"
EMAIL_HOST_USER: "verleih@fet.at"
EMAIL_HOST_PASSWORD: ""
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
@@ -45,7 +47,7 @@ services:
retries: 20 retries: 20
etherpad: etherpad:
container_name: etherpad-container container_name: etherpad-container
image: etherpad/etherpad:1.8.17 image: etherpad/etherpad:2.5.2
# ports: # ports:
# - 9001:9001 # - 9001:9001
environment: environment:
@@ -55,8 +57,9 @@ services:
DB_NAME: etherpaddb DB_NAME: etherpaddb
DB_USER: user DB_USER: user
DB_PASS: "hgu" DB_PASS: "hgu"
DB_CHARSET: utf8 DB_CHARSET: "utf8mb4"
TRUST_PROXY: false TRUST_PROXY: false
AUTHENTICATION_METHOD: "apikey"
depends_on: depends_on:
etherpadsql: etherpadsql:
condition: "service_healthy" condition: "service_healthy"
@@ -82,7 +85,8 @@ services:
MYSQL_CHARSET: utf8 MYSQL_CHARSET: utf8
MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes: volumes:
- mysql-volume:/docker-entrypoint-initdb.d/ - ./inits/django:/docker-entrypoint-initdb.d/
- ./databases/django:/var/lib/mysql:Z
networks: networks:
- django-db-network - django-db-network
healthcheck: healthcheck:
@@ -100,7 +104,8 @@ services:
MYSQL_CHARSET: utf8 MYSQL_CHARSET: utf8
MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes: volumes:
- etherpad-mysql-volume:/docker-entrypoint-initdb.d/ - ./init/etherpad:/docker-entrypoint-initdb.d/
- ./databases/etherpad:/var/lib/mysql:Z
networks: networks:
- etherpad-db-network - etherpad-db-network
healthcheck: healthcheck:

View File

@@ -1,6 +1,6 @@
from django.utils.version import get_version from django.utils.version import get_version
VERSION = (2, 2, 0, "final", 0) VERSION = (2, 2, 1, "final", 0)
BUILD = 0 BUILD = 0
__version__ = get_version(VERSION) __version__ = get_version(VERSION)

View File

@@ -18,6 +18,8 @@ env = environ.Env(
ETHERPAD_GROUP=(str, ""), ETHERPAD_GROUP=(str, ""),
GALLERY_PATH=(str, "uploads/gallery"), GALLERY_PATH=(str, "uploads/gallery"),
MC_MASTERPASSWORD=(str, ""), MC_MASTERPASSWORD=(str, ""),
EMAIL_HOST_USER=(str, ""),
EMAIL_HOST_PASSWORD=(str, ""),
) )
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@@ -103,6 +105,8 @@ else:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "buran.htu.tuwien.ac.at" EMAIL_HOST = "buran.htu.tuwien.ac.at"
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True EMAIL_USE_TLS = True

View File

@@ -204,7 +204,12 @@ class BillAdmin(admin.ModelAdmin):
actions = ["make_cleared", "make_finished"] actions = ["make_cleared", "make_finished"]
autocomplete_fields = ["resolution"] autocomplete_fields = ["resolution"]
list_filter = ["status", "affiliation", "payer", BillPeriodeFilter] list_filter = ["status", "affiliation", "payer", BillPeriodeFilter]
search_fields = ["purpose", "bankdata__name"] search_fields = [
"purpose",
"bankdata__name",
"bill_creator__firstname",
"bill_creator__surname",
]
show_facets = admin.ShowFacets.ALWAYS show_facets = admin.ShowFacets.ALWAYS
ordering = ["-id"] ordering = ["-id"]

View File

@@ -6,6 +6,7 @@ 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
from django.forms import DateInput from django.forms import DateInput
from django.utils import timezone
from members.models import Member from members.models import Member
@@ -412,7 +413,7 @@ class BillAdminForm(forms.ModelForm):
self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter( self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter(
( (
Q(option=Resolution.Option.FINANCE) Q(option=Resolution.Option.FINANCE)
& Q(date__gt=datetime.datetime.now(tz=datetime.UTC).date() - relativedelta(years=2)) & Q(date__gt=timezone.now().date() - relativedelta(years=2))
) )
| Q(option=Resolution.Option.PERMANENT) | Q(option=Resolution.Option.PERMANENT)
) )

View File

@@ -1,8 +1,8 @@
import datetime
import io import io
from pathlib import Path from pathlib import Path
from django.core.files import File from django.core.files import File
from django.utils import timezone
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter
from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814 from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814
@@ -34,7 +34,7 @@ def generate_pdf(wiref):
) )
# Get budget year # Get budget year
today = datetime.datetime.now(tz=datetime.UTC).date() today = timezone.now().date()
if today.month < 7: if today.month < 7:
budget_year = f"{today.year - 1}-{today.year}" budget_year = f"{today.year - 1}-{today.year}"
else: else:

View File

@@ -77,7 +77,7 @@ class EventForm(PostForm):
"image": "Verwendbare Formate: Bildformate", "image": "Verwendbare Formate: Bildformate",
"is_pinned": ( "is_pinned": (
"Dieses Event soll als erstes auf der Startseite angeheftet werden und sich " "Dieses Event soll als erstes auf der Startseite angeheftet werden und sich "
"automatisch einen Monat nach der Veröffentlichung wieder lösen." "automatisch ein Tag nach dem Eventende wieder lösen."
), ),
} }

View File

@@ -1,7 +1,9 @@
import calendar
import datetime import datetime
from django.db import models from django.db import models
from django.db.models import Case, Q, When from django.db.models import Case, Q, When
from django.utils import timezone
from .choices import PostType, Status from .choices import PostType, Status
@@ -81,21 +83,26 @@ class ArticleManager(PublishedManager, models.Manager):
def pinned(self, public=True): def pinned(self, public=True):
# Get date for pinned news that is max 1 month old. # Get date for pinned news that is max 1 month old.
post_date = datetime.datetime.now(tz=datetime.UTC).date() post_date = timezone.now().date()
__month = post_date.month _day = post_date.day
__year = post_date.year _month = post_date.month
_year = post_date.year
if __month != 1: if _month != 1:
__month -= 1 _month -= 1
else: else:
# If the current month is January, you get the date from December of previous year. # If the current month is January, you get the date from December of previous year.
__month = 12 _month = 12
__year -= 1 _year -= 1
post_date = post_date.replace(year=__year, month=__month) # Clamp day to last day of target month (handles 30/31 and Feb)
last_day = calendar.monthrange(_year, _month)[1]
safe_day = min(_day, last_day)
post_date = post_date.replace(year=_year, month=_month, day=safe_day)
# Get date for event posts that is max 1 day old. # Get date for event posts that is max 1 day old.
event_date = datetime.datetime.now(tz=datetime.UTC).date() - datetime.timedelta(1) event_date = timezone.now().date() - datetime.timedelta(1)
return ( return (
self.published(public) self.published(public)
@@ -145,7 +152,7 @@ class AllEventManager(PublishedManager, models.Manager):
return qs.order_by("-date") return qs.order_by("-date")
def future_events(self, public=True): def future_events(self, public=True):
date_today = datetime.datetime.now(tz=datetime.UTC).date() date_today = timezone.now().date()
qs = self.published(public).filter(event_start__gt=date_today) qs = self.published(public).filter(event_start__gt=date_today)
return qs.reverse() return qs.reverse()
@@ -166,12 +173,12 @@ class EventManager(PublishedManager, models.Manager):
return qs.order_by("-date") return qs.order_by("-date")
def future_events(self, public=True): def future_events(self, public=True):
date_today = datetime.datetime.now(tz=datetime.UTC).date() date_today = timezone.now().date()
qs = self.published(public).filter(event_start__gt=date_today) qs = self.published(public).filter(event_start__gt=date_today)
return qs.reverse() return qs.reverse()
def past_events(self, public=True): def past_events(self, public=True):
date_today = datetime.datetime.now(tz=datetime.UTC).date() date_today = timezone.now().date()
qs = self.published(public).filter(event_start__lt=date_today) qs = self.published(public).filter(event_start__lt=date_today)
return qs return qs
@@ -191,11 +198,11 @@ class FetMeetingManager(PublishedManager, models.Manager):
return qs.order_by("-date") return qs.order_by("-date")
def future_events(self): def future_events(self):
date_today = datetime.datetime.now(tz=datetime.UTC).date() date_today = timezone.now().date()
qs = self.published().filter(event_start__gt=date_today) qs = self.published().filter(event_start__gt=date_today)
return qs.reverse() return qs.reverse()
def past_events(self): def past_events(self):
date_today = datetime.datetime.now(tz=datetime.UTC).date() date_today = timezone.now().date()
qs = self.published().filter(event_start__lt=date_today) qs = self.published().filter(event_start__lt=date_today)
return qs return qs

View File

@@ -42,7 +42,7 @@ class RentalAdmin(admin.ModelAdmin):
("date_start", "date_end"), ("date_start", "date_end"),
"reason", "reason",
"rentalitems", "rentalitems",
"total_disposit", ("total_disposit", "intern"),
), ),
}, },
), ),
@@ -93,13 +93,10 @@ class RentalAdmin(admin.ModelAdmin):
obj.author = request.user obj.author = request.user
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
@admin.display(description="Kaution (EUR)") @admin.display(description="Kaution insgesamt")
def total_disposit(self, obj): def total_disposit(self, obj):
total_disposit = 0 total_disposit = obj.calc_total_deposit()
for elem in obj.rentalitems.all(): return f"{total_disposit}"
total_disposit += elem.deposit
return f"{total_disposit}"
@admin.register(RentalItem) @admin.register(RentalItem)

View File

@@ -51,6 +51,13 @@ class RentalAdminForm(forms.ModelForm):
widgets = {"rentalitems": forms.CheckboxSelectMultiple()} widgets = {"rentalitems": forms.CheckboxSelectMultiple()}
help_texts = {
"status": (
"Wird der Status auf 'Verleih genehmigt' oder 'Verleih abgelehnt' gesetzt, wird "
"eine E-Mail gesendet."
),
}
class RentalItemAdminForm(forms.ModelForm): class RentalItemAdminForm(forms.ModelForm):
class Meta: class Meta:

View File

@@ -1,28 +1,28 @@
import logging import logging
from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
RENTAL_EMAIL = "verleih@fet.at" RENTAL_EMAIL = settings.EMAIL_HOST_USER
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def send_mail_approved(obj): def send_mail_approved(obj):
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}"
total_deposit = obj.calc_total_deposit()
total_deposit = 0
for rentalitem in obj.rentalitems.all():
total_deposit += rentalitem.deposit
message = ( message = (
f"Hallo {obj.firstname},\n" f"Hallo {obj.firstname},\n\n"
f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die " f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die "
f"Gegenstände können am {obj.date_start.strftime('%d.%m.%Y')} während der Beratungszeit " f"Gegenstände können am {obj.date_start.strftime('%d.%m.%Y')} während der Beratungszeit "
"(Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden. Bitte bring " "(Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden.\n"
f"den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n"
"Liebe Grüße,\n"
"das Verleih-Team"
) )
if total_deposit > 0:
message += f"Bitte bring den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n"
message += "\nLiebe Grüße,\ndas Verleih-Team"
email = EmailMessage( email = EmailMessage(
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
) )
@@ -30,15 +30,15 @@ def send_mail_approved(obj):
try: try:
email.send() email.send()
except Exception as exc: except Exception as exc:
logger.error("Failed to send approval email for rental #%s. Error: %s", obj.id, exc) logger.info("Failed to send approval email for rental #%s. Error: %s", obj.id, exc)
def send_mail_rejected(obj): def send_mail_rejected(obj):
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}"
message = ( message = (
f"Hallo {obj.firstname},\n" f"Hallo {obj.firstname},\n\n"
f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n" f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n\n"
"Liebe Grüße,\n" "Liebe Grüße,\n"
"das Verleih-Team" "das Verleih-Team"
) )
@@ -50,4 +50,4 @@ def send_mail_rejected(obj):
try: try:
email.send() email.send()
except Exception as exc: except Exception as exc:
logger.error("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc) logger.info("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc)

View File

@@ -0,0 +1,6 @@
from django.db import models
class RentalItemsManager(models.Manager):
def get_queryset(self):
return super().get_queryset().order_by("name")

View File

@@ -0,0 +1,55 @@
# Generated by Django 5.2.7 on 2025-10-30 12:05
import django.core.validators
import rental.validators
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='RentalItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('description', models.CharField(blank=True, default='', max_length=128, verbose_name='Beschreibung')),
('deposit', models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='Kaution (EUR)')),
('image', models.ImageField(blank=True, null=True, upload_to='rentalitems', verbose_name='Bild')),
('induction', models.BooleanField(default=False, verbose_name='Einschulung notwendig')),
('location', models.CharField(blank=True, default='', max_length=128, verbose_name='Standort')),
],
options={
'verbose_name': 'Verleihgegenstand',
'verbose_name_plural': 'Verleihgegenstände',
},
),
migrations.CreateModel(
name='Rental',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('firstname', models.CharField(max_length=128, verbose_name='Vorname')),
('surname', models.CharField(max_length=128, verbose_name='Nachname')),
('matriculation_number', models.CharField(max_length=8, verbose_name='Matrikelnummer')),
('email', models.EmailField(max_length=254, verbose_name='E-Mail')),
('phone', models.CharField(max_length=32, validators=[rental.validators.PhoneNumberValidator()], verbose_name='Telefonnummer')),
('organization', models.CharField(max_length=128, verbose_name='Organisation')),
('date_start', models.DateField(verbose_name='Abholdatum')),
('date_end', models.DateField(verbose_name='Rückgabedatum')),
('reason', models.TextField(max_length=500, verbose_name='Grund der Ausleihe')),
('comment', models.TextField(blank=True, default='', max_length=500, verbose_name='Kommentar')),
('file_field', models.FileField(blank=True, null=True, upload_to='uploads/rental/rental/', validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='Verleihformular')),
('status', models.CharField(choices=[('S', 'Eingereicht'), ('A', 'Verleih genehmigt'), ('J', 'Verleih abgelehnt'), ('I', 'Verleihgegenstände ausgegeben'), ('R', 'Verleihgegenstände zurückgegeben')], default='S', max_length=1, verbose_name='Status')),
('rentalitems', models.ManyToManyField(to='rental.rentalitem', verbose_name='Verleihgegenstände')),
],
options={
'verbose_name': 'Verleih',
'verbose_name_plural': 'Verleih',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-30 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rental', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='rental',
name='intern',
field=models.BooleanField(default=False, verbose_name='Interner Verleih'),
),
]

View File

View File

@@ -3,6 +3,7 @@ from django.db import models
from django.forms import ValidationError from django.forms import ValidationError
from .mails import send_mail_approved, send_mail_rejected from .mails import send_mail_approved, send_mail_rejected
from .managers import RentalItemsManager
from .validators import PhoneNumberValidator from .validators import PhoneNumberValidator
@@ -22,6 +23,8 @@ class RentalItem(models.Model):
location = models.CharField(verbose_name="Standort", max_length=128, blank=True, default="") location = models.CharField(verbose_name="Standort", max_length=128, blank=True, default="")
objects = RentalItemsManager()
class Meta: class Meta:
verbose_name = "Verleihgegenstand" verbose_name = "Verleihgegenstand"
verbose_name_plural = "Verleihgegenstände" verbose_name_plural = "Verleihgegenstände"
@@ -41,6 +44,7 @@ class Rental(models.Model):
) )
organization = models.CharField(verbose_name="Organisation", max_length=128) organization = models.CharField(verbose_name="Organisation", max_length=128)
intern = models.BooleanField(verbose_name="Interner Verleih", default=False)
date_start = models.DateField(verbose_name="Abholdatum") date_start = models.DateField(verbose_name="Abholdatum")
date_end = models.DateField(verbose_name="Rückgabedatum") date_end = models.DateField(verbose_name="Rückgabedatum")
@@ -107,3 +111,12 @@ class Rental(models.Model):
if self.date_start > self.date_end: if self.date_start > self.date_end:
raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.") raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.")
def calc_total_deposit(self) -> int:
total_deposit = 0
if not self.intern:
for item in self.rentalitems.all():
total_deposit += item.deposit
return total_deposit

View File

@@ -10,7 +10,7 @@ urlpatterns = [
path("overview/", RentalListView.as_view(), name="index"), path("overview/", RentalListView.as_view(), name="index"),
path("request-rental/", RentalCreateView.as_view(), name="rental_create"), path("request-rental/", RentalCreateView.as_view(), name="rental_create"),
path( path(
"request-rental/<int:pk>/done/", "request-rental/done/",
RentalCreateDoneView.as_view(), RentalCreateDoneView.as_view(),
name="rental_create_done", name="rental_create_done",
), ),

View File

@@ -1,6 +1,6 @@
import io import io
from pathlib import Path
from django.contrib.staticfiles import finders
from django.core.files import File from django.core.files import File
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter
@@ -27,27 +27,21 @@ def generate_rental_pdf(rental: Rental) -> bool:
}, },
) )
total_deposit = 0
for i, item in enumerate(rental.rentalitems.all(), start=1): for i, item in enumerate(rental.rentalitems.all(), start=1):
total_deposit += item.deposit
data.update( data.update(
{ {
f"Produkt Row{i}": item.name, f"Produkt Row{i}": item.name,
f"Menge Row{i}": "1", f"Menge Row{i}": "1",
f"Kaution Row{i}": item.deposit, f"Kaution Row{i}": (str(item.deposit) if not rental.intern else "0"),
}, },
) )
data.update( total_deposit = rental.calc_total_deposit()
{ data.update({"Gesamtkaution": str(total_deposit)})
"Gesamtkaution": total_deposit,
},
)
# Write data in pdf # Write data in pdf
pdf_path = Path(Path(__file__).parent) / "static/rental/Verleihformular.pdf" pdf_path_str = finders.find("rental/Verleihformular.pdf")
reader = PdfReader(pdf_path) reader = PdfReader(pdf_path_str)
writer = PdfWriter() writer = PdfWriter()
writer.append(reader) writer.append(reader)
@@ -58,6 +52,7 @@ def generate_rental_pdf(rental: Rental) -> bool:
with io.BytesIO() as bytes_stream: with io.BytesIO() as bytes_stream:
writer.write(bytes_stream) writer.write(bytes_stream)
bytes_stream.seek(0)
# Save pdf in rental # Save pdf in rental
rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf" rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf"

View File

@@ -3,8 +3,10 @@ import datetime
from datetime import date from datetime import date
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
@@ -12,6 +14,9 @@ from django.views.generic.edit import CreateView
from .forms import RentalCreateForm from .forms import RentalCreateForm
from .models import Rental, RentalItem from .models import Rental, RentalItem
# Maximum number of rental items per rental entry because of table size limitations in PDF file
RENTAL_ITEMS_MAX = 5
def _calc_days_from_current_month(month: date) -> list: def _calc_days_from_current_month(month: date) -> list:
last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1]) last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1])
@@ -60,7 +65,7 @@ def _get_display_period(view_type: str, period: str) -> date:
) )
except Exception: except Exception:
# Get first day of the current week # Get first day of the current week
today = datetime.datetime.now(tz=datetime.UTC).date() today = timezone.now().date()
display_date = today - datetime.timedelta(days=today.weekday()) display_date = today - datetime.timedelta(days=today.weekday())
# Handle month view # Handle month view
@@ -72,7 +77,7 @@ def _get_display_period(view_type: str, period: str) -> date:
) )
except Exception: except Exception:
# Get the first day of the current month # Get the first day of the current month
display_date = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) display_date = timezone.now().date().replace(day=1)
return display_date return display_date
@@ -166,7 +171,7 @@ class RentalListView(ListView):
context["week_num"] = week_num context["week_num"] = week_num
# Get the current date for the calendar # Get the current date for the calendar
context["today"] = datetime.datetime.now(tz=datetime.UTC).date() context["today"] = timezone.now().date()
# Add rental items to the context for the filter # Add rental items to the context for the filter
context["rentalitems"] = RentalItem.objects.all() context["rentalitems"] = RentalItem.objects.all()
@@ -185,7 +190,7 @@ class RentalListView(ListView):
if rental["rentalitems__name"] not in rental_dict[day]: if rental["rentalitems__name"] not in rental_dict[day]:
rental_dict[day].append(rental["rentalitems__name"]) rental_dict[day].append(rental["rentalitems__name"])
context["rental_dict"] = rental_dict context["rental_dict"] = {k: sorted(v, key=str.casefold) for k, v in rental_dict.items()}
return context return context
@@ -223,6 +228,35 @@ class RentalCreateView(CreateView):
model = Rental model = Rental
template_name = "rental/create.html" template_name = "rental/create.html"
def form_valid(self, form):
# Get unsaved base Rental with all form data
base_rental = form.save(commit=False)
items = list(form.cleaned_data["rentalitems"])
selected_items = sorted(items, key=lambda x: x.name.lower())
for i in range(0, len(selected_items), RENTAL_ITEMS_MAX):
batch = selected_items[i : i + RENTAL_ITEMS_MAX]
# Clone base_rental — copying all its field values
new_rental = Rental.objects.create(
firstname=base_rental.firstname,
surname=base_rental.surname,
matriculation_number=base_rental.matriculation_number,
email=base_rental.email,
phone=base_rental.phone,
organization=base_rental.organization,
date_start=base_rental.date_start,
date_end=base_rental.date_end,
reason=base_rental.reason,
comment=base_rental.comment,
status=base_rental.status,
)
# Important: Add M2M relations after the object has been saved
new_rental.rentalitems.add(*batch)
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -231,26 +265,12 @@ class RentalCreateView(CreateView):
return context return context
def get_success_url(self): def get_success_url(self):
return reverse("rental:rental_create_done", kwargs={"pk": self.object.pk}) return reverse("rental:rental_create_done")
class RentalCreateDoneView(TemplateView): class RentalCreateDoneView(TemplateView):
template_name = "rental/create_done.html" template_name = "rental/create_done.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
obj = Rental.objects.get(pk=self.kwargs["pk"])
total_deposit = 0
for elem in obj.rentalitems.all():
total_deposit += elem.deposit
context["object"] = obj
context["total_deposit"] = total_deposit
return context
class RentalItemDetailView(DetailView): class RentalItemDetailView(DetailView):
model = RentalItem model = RentalItem

View File

@@ -5,13 +5,18 @@
{% block content %} {% block content %}
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1 max-w-2xl"> <main class="container mx-auto w-full px-4 my-8 flex-1 max-w-2xl">
<section class="block w-full"> <section class="block w-full">
<p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de"> <p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
Deine Verleihanfrage mit der Nummer #{{ pk }} wurde erfolgreich eingereicht. Die Gegenstände können am {{ object.date_start }} während der Beratungszeit (Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden. Bitte bring den Gesamtpfand in Höhe von {{ total_deposit }} € in bar mit. Deine Verleihanfrage ist eingegangen - danke dir! 🎉
</p> Wir kümmern uns jetzt darum und melden uns per E-Mail mit den nächsten Schritten.
<div class="mt-10 flex items-center justify-center"> </p>
<a href="{% url 'home' %}" type="submit" class="block btn btn-primary">Zur Startseite</a> <p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
</div> Kleiner Hinweis: Bevor du die Sachen abholen kannst, wird deine Anfrage kurz geprüft und freigegeben.
</section> Sobald alles genehmigt ist, bekommst du von uns eine Mail.
</p>
<div class="mt-10 flex items-center justify-center">
<a href="{% url 'home' %}" type="submit" class="block btn btn-primary">Zur Startseite</a>
</div>
</section>
</main> </main>
{% endblock content %} {% endblock content %}