diff --git a/.idea/misc.xml b/.idea/misc.xml index 7c7709e01ee2155b7c0a4e707b518b02f2274eb5..f967d638a7fcfd76693951f28f989c088f769539 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,5 +6,5 @@ <component name="NodePackageJsonFileManager"> <packageJsonPaths /> </component> - <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (school-apps)" project-jdk-type="Python SDK" /> + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (SchoolApps)" project-jdk-type="Python SDK" /> </project> \ No newline at end of file diff --git a/.idea/school-apps.iml b/.idea/school-apps.iml index 60fb427d477958236a96ce6b6d249cbf6097e7a2..96873b7da10fa79cb630f41924905744bec63b0c 100644 --- a/.idea/school-apps.iml +++ b/.idea/school-apps.iml @@ -23,7 +23,7 @@ <excludeFolder url="file://$MODULE_DIR$/env" /> <excludeFolder url="file://$MODULE_DIR$/schoolapps/staticcollect" /> </content> - <orderEntry type="jdk" jdkName="Python 3.7 (school-apps)" jdkType="Python SDK" /> + <orderEntry type="jdk" jdkName="Python 3.7 (SchoolApps)" jdkType="Python SDK" /> <orderEntry type="sourceFolder" forTests="false" /> </component> <component name="TemplatesService"> diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ffa7302ab357de373c50ef91f1175f443043f35..2dc8e229104aee482fd7bba92d1c47979d611d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,4 +80,7 @@ Weitere Verbesserungen der vorangegeangen Versionen dieses Releases. ## 1.1.3 "Aebli" * Bugfix in REBUS (Submit wieder erlaubt) (#335) -* Klassenlehrkräfte werden im Plan angezeigt (#334) \ No newline at end of file +* Klassenlehrkräfte werden im Plan angezeigt (#334) + +## 1.1.4 "Aebli" +* Bugfix in der PWA (Offline-Fallback-Page wird nicht mehr bei teils aktiver Netzwerkverbindung fälschlicherweise angezeigt) \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index 64d0f5baf2e338d30adb42f025ece35dc18c5e7a..05e7f01bb908b0de69941f43a75cd935c2a443bd 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -33,7 +33,6 @@ GRANT SELECT ON Untis.* TO 'www-data'@'localhost'; ### UNTIS-Beispieldaten importieren Zum Testen kann die Datei `untiskath.sql` vom Forum in die Datenbank `Untis` importiert werden. - ### SchoolApps clonen ``` git clone git@github.com:Katharineum/school-apps.git @@ -49,7 +48,6 @@ pip install -r requirements.txt - `example_secure_settings.py` zu `secure_settings.py` kopieren und anpassen (hier müssen auch die passenden DB-Zugangsdaten eingetragen werden) - ### Migrations durchführen/auflösen Leider kommt es bei einer Erstinstallation von SchoolApps immer noch zu Problemen mit den Migrations. Sollte es Schwierigkeiten geben, @hansegucker kontaktieren. diff --git a/README.md b/README.md index d95f8391b9751c61248844e996ebab6516e7e3ed..731e35747099e550067dcbffad69d490de7b8d3b 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ SchoolApps ist ein modulares System zum Bereitstellen von speziellen Anwendungen für den Schulalltag. Besonderer Fokus liegt dabei auf dem Stundenplansystem. -© 2018 by [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank-Poetzsch-Heffter](mailto:p-h@katharineum.de) +© 2018 by [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank Poetzsch-Heffter](mailto:p-h@katharineum.de) -© 2019 by [Hangzhi Yu](mailto:yuha@katharineum.de), [Julian Leucker](mailto:leuckeju@katharineum.de), [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank-Poetzsch-Heffter](mailto:p-h@katharineum.de) \ No newline at end of file +© 2019 by [Hangzhi Yu](mailto:yuha@katharineum.de), [Julian Leucker](mailto:leuckeju@katharineum.de), [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank Poetzsch-Heffter](mailto:p-h@katharineum.de) diff --git a/schoolapps/aub/forms.py b/schoolapps/aub/forms.py index 245e2619bd5b7d2afa9cec7906fd1c61fd0ea9cd..1b59bbffc20e41edab82c12146d5e75e7763c9fa 100755 --- a/schoolapps/aub/forms.py +++ b/schoolapps/aub/forms.py @@ -4,6 +4,7 @@ from django.utils import timezone from datetime import datetime from material import Layout, Row, Fieldset from aub.models import Aub +# from tinymce.widgets import TinyMCE class ApplyForAUBForm(forms.ModelForm): @@ -17,6 +18,7 @@ class ApplyForAUBForm(forms.ModelForm): to_date = forms.DateField(label='Datum', input_formats=['%d.%m.%Y']) to_lesson = forms.ChoiceField(label='Stunde', choices=lessons, required=False, widget=forms.Select(attrs={'onchange': 'setTime(this)'})) to_time = forms.TimeField(label='Zeit', input_formats=['%H:%M'], initial=initial_to_time) + # description = forms.CharField(label='Bitte begründen Sie Ihren Antrag.', widget=TinyMCE(attrs={'cols': 80, 'rows': 30})) description = forms.CharField(label='Bitte begründen Sie Ihren Antrag.') layout = Layout(Fieldset('Von', @@ -33,6 +35,9 @@ class ApplyForAUBForm(forms.ModelForm): model = Aub fields = ('id', 'from_date', 'from_time', 'to_date', 'to_time', 'description') + class Media: + js = ('/media/tinymce/jscripts/tiny_mce/tiny_mce.js', '',) + def clean(self): cleaned_data = super().clean() diff --git a/schoolapps/aub/models.py b/schoolapps/aub/models.py index e9193afc05c98914f97f43ede0cfe91fdc08af1a..7366bd652640e2b1b84de11b692cd4549a126cff 100755 --- a/schoolapps/aub/models.py +++ b/schoolapps/aub/models.py @@ -45,7 +45,6 @@ class Aub(models.Model): created_at = models.DateTimeField(default=timezone.now, verbose_name="Erstellungszeitpunkt") def getStatus(self): - print(self.status, self.created_by, self.id) return status_list[self.status] def __str__(self): diff --git a/schoolapps/aub/templates/aub/index.html b/schoolapps/aub/templates/aub/index.html index 59defc97477ddb85664572ee5463a189c971d488..ce9d67f5818fa788249e524cf97fa8d164471b0e 100755 --- a/schoolapps/aub/templates/aub/index.html +++ b/schoolapps/aub/templates/aub/index.html @@ -33,7 +33,7 @@ <div class="col s12 m4"> <p> {% if aub.status == 0 %} - <form action="{% url 'aub_edit' aub.id %}" class="right"> + <form action="{% url 'aub_edit' aub.id %}" class="left"> {% csrf_token %} <button type="submit" name="edit" class="waves-effect waves-light btn-flat btn-flat-medium" title="Bearbeiten"> <i class="material-icons center green-text">create</i> @@ -41,7 +41,7 @@ </form> {% endif %} {% if aub.status == 0 or aub.status == 1 %} - <form action="" method="POST" class="right"> + <form action="" method="POST" class="left"> {% csrf_token %} <input type="hidden" value="{{ aub.id }}" name="aub-id"> <button type="submit" onclick="return confirm('Wollen Sie den Antrag wirklich löschen?')" name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" title="Löschen"> diff --git a/schoolapps/aub/views.py b/schoolapps/aub/views.py index ba1a759e7cd7b711d3da9ee0a2058375d98c798e..578c8d89c0dd4cc9c9fe8e808f05e4c243198321 100755 --- a/schoolapps/aub/views.py +++ b/schoolapps/aub/views.py @@ -196,7 +196,10 @@ def check2(request): def archive(request): order_crit = '-from_date' if 'created_by' in request.GET: - item = int(request.GET['created_by']) + try: + item = int(request.GET['created_by']) + except ValueError: + item = '' aub_list = Aub.objects.filter((Q(status__exact=2) | Q(status__exact=3)) & Q(created_by=item)).order_by(order_crit) else: aub_list = Aub.objects.filter(Q(status__exact=2) | Q(status__exact=3)).order_by(order_crit) diff --git a/schoolapps/dashboard/models.py b/schoolapps/dashboard/models.py index 3aaf75ae4c872092683b26809d6cbbc0155da8b1..8a26ce363f72ef0867614a389fc1c7b1062ee16f 100755 --- a/schoolapps/dashboard/models.py +++ b/schoolapps/dashboard/models.py @@ -11,7 +11,7 @@ from mailer import send_mail_with_template class Activity(models.Model): user = models.ForeignKey(to=User, on_delete=models.CASCADE) - title = models.CharField(max_length=200) + title = models.CharField(max_length=150) description = models.TextField(max_length=500) app = models.CharField(max_length=100) @@ -24,7 +24,7 @@ class Activity(models.Model): class Notification(models.Model): user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="notifications") - title = models.CharField(max_length=200) + title = models.CharField(max_length=150) description = models.TextField(max_length=500) link = models.URLField(blank=True) @@ -48,8 +48,8 @@ def register_notification(user, title, description, app="SchoolApps", link=""): class Cache(models.Model): - id = models.CharField(max_length=200, unique=True, primary_key=True, verbose_name="ID") - name = models.CharField(max_length=200, verbose_name="Name") + id = models.CharField(max_length=150, unique=True, primary_key=True, verbose_name="ID") + name = models.CharField(max_length=150, verbose_name="Name") expiration_time = models.IntegerField(default=20, verbose_name="Ablaufzeit") last_time_updated = models.DateTimeField(blank=True, null=True, verbose_name="Letzter Aktualisierungszeitpunkt des Caches") diff --git a/schoolapps/fibu/README.md b/schoolapps/fibu/README.md new file mode 100755 index 0000000000000000000000000000000000000000..bca3b98cf3f604d0009e59c57ce83a6fd698f3c8 --- /dev/null +++ b/schoolapps/fibu/README.md @@ -0,0 +1,30 @@ +# SchoolApps: FiBu (Finanzbuchhaltung) +## Workflow + +1. Berechtigungen setzen + + Anwender: request_booking + + Buchhalter: manage_booking + + Manager: manage_costcenter, manage_account + +2. Manager richtet Kostenstellen (z.B. Schulträger-konsumtiv, +Schulträger-investiv, Elternverein, Fundraising, ...) sowie +Buchungskonten (z.B. Klassenraum-Festwert, Geschäftszimmer, +Fachschaft Mathematik, ...) ein und setzt dabei Budgets fest, +die in der Summe den Haushaltsplan ergeben. + +3. Anwender stellt Anträge für Ausgaben. + +4. Manager bewilligt Anträge unter Zuordnung zum passenden +Buchungskonto oder lehnt sie ab. + +5. Anwender gibt für bewilligte Anträge Bestellung auf und +ändert den Status der Buchung entsprechend. + +6. Anwender reicht Rechnung im Geschäftszimmer ein und ändert +den Status der Buchung entsprechend. + +7. Buchhalter vervollständigt die Buchung, weist die Rechnung +an und ändert den Status auf bezahlt. + +8. Buchhalter und Manager kontrollieren durch Aufruf von Berichten +den Haushalt. \ No newline at end of file diff --git a/schoolapps/fibu/__init__.py b/schoolapps/fibu/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/schoolapps/fibu/admin.py b/schoolapps/fibu/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..9f16fa184564f6a7a31fcaec3458e6e16b26c43c --- /dev/null +++ b/schoolapps/fibu/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import CostCenter, Account, Booking + +admin.site.register(CostCenter) +admin.site.register(Account) +admin.site.register(Booking) diff --git a/schoolapps/fibu/apps.py b/schoolapps/fibu/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..f98393e1c0883b1ad219582feb398b0381e45263 --- /dev/null +++ b/schoolapps/fibu/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FibuConfig(AppConfig): + name = 'fibu' diff --git a/schoolapps/fibu/decorators.py b/schoolapps/fibu/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..63210079b3262886d437ca6cd785810f6ac9c7df --- /dev/null +++ b/schoolapps/fibu/decorators.py @@ -0,0 +1,24 @@ +from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth import REDIRECT_FIELD_NAME + +from .models import Booking + + +# prevent to show booking details from foreign users +def check_own_booking_verification(user): + return Booking.objects.all().filter(created_by=user) + + +def check_own_booking(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): + """ + Decorator for views that checks that the user only gets his own bookings, redirecting + to the dashboard if necessary. + """ + actual_decorator = user_passes_test( + check_own_booking_verification, + login_url=login_url, + redirect_field_name=redirect_field_name + ) + if function: + return actual_decorator(function) + return actual_decorator diff --git a/schoolapps/fibu/filters.py b/schoolapps/fibu/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ca80862169f0d0fdd037163f23b0ffdfaedd34 --- /dev/null +++ b/schoolapps/fibu/filters.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import User +import django_filters +from .models import Booking +from django.db.utils import ProgrammingError + + +def get_fibu_users(): + """ Find all users who requests a boooking """ + try: + fibu_users = Booking.objects.values_list('contact') + users = list(User.objects.filter(id__in=fibu_users)) + # user_ids = [(str(user.id),user.username) for user in users] + user_ids = [(str(user.id), user.last_name + ', ' + user.first_name) for user in users] + user_ids_sorted = sorted(user_ids, key=lambda user: user[1]) + return user_ids_sorted + except ProgrammingError: + return [] + + +class BookingFilter(django_filters.FilterSet): + contact = django_filters.ChoiceFilter(label='Von', choices=get_fibu_users()) + + class Meta: + model = Booking + fields = ['contact', ] diff --git a/schoolapps/fibu/forms.py b/schoolapps/fibu/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..9a4854092ee7068770b0275eee4f3b3ab6887471 --- /dev/null +++ b/schoolapps/fibu/forms.py @@ -0,0 +1,62 @@ +from django import forms +from django.utils import timezone +from material import Layout, Row, Fieldset + +from .models import Booking, CostCenter, Account + + +class SimpleBookingForm(forms.ModelForm): + description = forms.CharField(label='Beschreibung – Was soll angeschafft werden?') + planned_amount = forms.IntegerField( + label='Erwarteter Betrag – Welcher Betrag ist erforderlich?', help_text="in Euro, ohne Komma") + justification = forms.CharField(label='Begründung – Begründe ggf. deinen Antrag.', required=False) + + layout = Layout(Row('description', 'planned_amount'), Row('justification')) + + class Meta: + model = Booking + fields = ['id', 'description', 'planned_amount', 'justification'] + + +class CheckBookingForm(forms.ModelForm): + account = forms.ModelChoiceField(Account.objects.filter().order_by('cost_center', 'name')) + + class Meta: + model = Booking + fields = ['account', ] + + +class CompleteBookingForm(forms.ModelForm): + accounts = Account.objects.filter().order_by('cost_center', 'name') + account = forms.ModelChoiceField(queryset=accounts) + submission_date = forms.DateField(label='Bearbeitungsdatum', initial=timezone.now()) + + layout = Layout(Fieldset("Allgemeines", + Row('description', 'justification'), + Row("contact", "planned_amount"), + Row('account', 'status') + ), + Fieldset('Details', + Row('firma', 'invoice_number', 'amount'), + Row('invoice_date', 'maturity', 'submission_date', 'booking_date'), + Row('payout_number', 'upload') + ) + ) + + class Meta: + model = Booking + fields = ['id', 'description', 'planned_amount', 'justification', 'account', 'contact', 'invoice_date', + 'invoice_number', 'firma', 'amount', 'submission_date', 'payout_number', 'booking_date', + 'maturity', 'upload', 'status'] + + +class CostCenterForm(forms.ModelForm): + class Meta: + model = CostCenter + fields = ['id', 'name', 'year'] + + +class AccountForm(forms.ModelForm): + class Meta: + model = Account + fields = ['id', 'name', 'cost_center', 'income', 'budget'] diff --git a/schoolapps/fibu/migrations/0001_initial.py b/schoolapps/fibu/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..acff540c447b1c52a06949b1e1a9b78d20edded1 --- /dev/null +++ b/schoolapps/fibu/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 2.2.8 on 2019-12-27 06:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('income', models.BooleanField(default=False)), + ('budget', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ('saldo', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ('rest', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ], + options={ + 'permissions': [('manage_account', 'Can manage account')], + }, + ), + migrations.CreateModel( + name='CostCenter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('year', models.IntegerField(choices=[(2020, '2020'), (2021, '2021'), (2022, '2022'), (2023, '2023')], + default=2019, verbose_name='Jahr')), + ], + options={ + 'permissions': [('manage_costcenter', 'Can manage costcenter')], + }, + ), + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('invoice_date', models.DateField(default='2000-12-31')), + ('invoice_number', models.CharField(default='0', max_length=20)), + ('firma', models.CharField(default='', max_length=30)), + ('description', models.CharField(max_length=50)), + ('amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ('planned_amount', models.IntegerField()), + ('submission_date', models.DateField(default='2000-12-31')), + ('justification', models.CharField(blank=True, max_length=2000, null=True)), + ('payout_number', models.IntegerField(default=0)), + ('booking_date', models.DateField(default='2000-12-31')), + ('maturity', models.DateField(default='2000-12-31')), + ('upload', models.FileField(blank=True, default=None, null=True, upload_to='uploads/fibu/%Y/')), + ('status', models.IntegerField(choices=[(0, 'beantragt'), (1, 'bewilligt'), (2, 'abgelehnt'), (3, 'bestellt'), (4, 'eingereicht'), (5, 'bezahlt')], default=0, verbose_name='Status')), + ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='fibu.Account')), + ('contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')), + ], + options={ + 'permissions': [('manage_booking', 'Can manage bookings'), ('request_booking', 'Can request a booking')], + }, + ), + migrations.AddField( + model_name='account', + name='costcenter', + field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to='fibu.CostCenter'), + ), + ] diff --git a/schoolapps/fibu/migrations/0002_auto_20191228_1357.py b/schoolapps/fibu/migrations/0002_auto_20191228_1357.py new file mode 100644 index 0000000000000000000000000000000000000000..50c6c27ac779af2362fd1a5ca6372f0ea76f7694 --- /dev/null +++ b/schoolapps/fibu/migrations/0002_auto_20191228_1357.py @@ -0,0 +1,90 @@ +# Generated by Django 2.2.6 on 2019-12-28 12:57 + +import datetime +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('fibu', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='account', + options={'permissions': [('manage_account', 'Can manage account')], 'verbose_name': 'Buchungskonto', + 'verbose_name_plural': 'Buchungskonten'}, + ), + migrations.AlterModelOptions( + name='booking', + options={ + 'permissions': [('manage_booking', 'Can manage bookings'), ('request_booking', 'Can request a booking'), + ('check_booking', 'Can check bookings')], 'verbose_name': 'Buchung', + 'verbose_name_plural': 'Buchungen'}, + ), + migrations.AlterModelOptions( + name='costcenter', + options={'permissions': [('manage_costcenter', 'Can manage costcenter')], 'verbose_name': 'Kostenstelle', + 'verbose_name_plural': 'Kostenstellen'}, + ), + migrations.AlterField( + model_name='account', + name='budget', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=9, verbose_name='Budget'), + ), + migrations.AlterField( + model_name='account', + name='costcenter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fibu.CostCenter', + verbose_name='Kostenstelle'), + ), + migrations.AlterField( + model_name='account', + name='income', + field=models.BooleanField(default=False, verbose_name='Einnahmekonto'), + ), + migrations.AlterField( + model_name='account', + name='name', + field=models.CharField(max_length=20, verbose_name='Buchungskonto'), + ), + migrations.AlterField( + model_name='booking', + name='booking_date', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='booking', + name='invoice_date', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='booking', + name='maturity', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='booking', + name='status', + field=models.IntegerField( + choices=[(0, 'beantragt'), (1, 'abgelehnt'), (2, 'bewilligt'), (3, 'bestellt'), (4, 'eingereicht'), + (5, 'bezahlt')], default=0, verbose_name='Status'), + ), + migrations.AlterField( + model_name='booking', + name='submission_date', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='costcenter', + name='name', + field=models.CharField(max_length=30, verbose_name='Kostenstelle'), + ), + migrations.AlterField( + model_name='costcenter', + name='year', + field=models.IntegerField(choices=[(2019, '2019'), (2020, '2020'), (2021, '2021'), (2022, '2022')], + default=2019, verbose_name='Jahr'), + ), + ] diff --git a/schoolapps/fibu/migrations/0003_auto_20191228_1553.py b/schoolapps/fibu/migrations/0003_auto_20191228_1553.py new file mode 100644 index 0000000000000000000000000000000000000000..6674b3d95d25eb5451050e7c10fefb65149ead28 --- /dev/null +++ b/schoolapps/fibu/migrations/0003_auto_20191228_1553.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.8 on 2019-12-28 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fibu', '0002_auto_20191228_1357'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='budget', + field=models.IntegerField(default=0, verbose_name='Budget'), + ), + ] diff --git a/schoolapps/fibu/migrations/__init__.py b/schoolapps/fibu/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/schoolapps/fibu/models.py b/schoolapps/fibu/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8611ca69c7738929db51b8cd97a02da7f8c5cb5e --- /dev/null +++ b/schoolapps/fibu/models.py @@ -0,0 +1,108 @@ +from django.utils import timezone +from django.db import models +from django.contrib.auth.models import User +from datetime import date + +current_year = timezone.now().year +YEARS = [(x, str(x)) for x in range(current_year, current_year + 4)] + + +class Status: + def __init__(self, name, style_class): + self.name = name + self.style_class = style_class + + def __str__(self): + return self.name + + +status_list = [ + Status(name='beantragt', style_class='red'), + Status(name='abgelehnt', style_class='black'), + Status(name='bewilligt', style_class='orange'), + Status(name='bestellt', style_class='yellow darken-1'), + Status(name='eingereicht', style_class='blue'), + Status(name='bezahlt', style_class='green'), +] + +status_choices = [(x, val.name) for x, val in enumerate(status_list)] + + +class CostCenter(models.Model): + # Kostenstellen z.B. Schulträger-konsumtiv, Schulträger-investiv, Elternverein, ... + name = models.CharField(max_length=30, blank=False, verbose_name="Kostenstelle") + year = models.IntegerField(default=timezone.now().year, choices=YEARS, blank=False, verbose_name="Jahr") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Kostenstelle" + verbose_name_plural = "Kostenstellen" + permissions = [ + ('manage_costcenter', 'Can manage costcenter'), + ] + + +class Account(models.Model): + # Buchungskonten, z.B. Fachschaften, Sekretariat, Schulleiter, Kopieren, Tafelnutzung + name = models.CharField(max_length=20, blank=False, verbose_name="Buchungskonto") + cost_center = models.ForeignKey(to=CostCenter, on_delete=models.CASCADE, blank=False, verbose_name="Kostenstelle") + income = models.BooleanField(default=False, + verbose_name="Einnahmekonto") # True, wenn es sich um ein Einnahmekonto handelt + budget = models.IntegerField(default=0, verbose_name="Budget") + saldo = models.DecimalField(max_digits=9, decimal_places=2, default=0.00) + rest = models.DecimalField(max_digits=9, decimal_places=2, default=0.00) + + def __str__(self): + return "{}: {}".format(self.cost_center, self.name) + + class Meta: + verbose_name = "Buchungskonto" + verbose_name_plural = "Buchungskonten" + permissions = [ + ('manage_account', 'Can manage account'), + ] + + +class Booking(models.Model): + # General information + account = models.ForeignKey(to=Account, on_delete=models.SET_NULL, blank=True, null=True, + verbose_name="Buchungskonto") + contact = models.ForeignKey(to=User, related_name='bookings', on_delete=models.SET_NULL + , verbose_name="Erstellt von", blank=True, null=True) + description = models.CharField(max_length=50, verbose_name="Beschreibung") + justification = models.CharField(max_length=2000, blank=True, null=True, verbose_name="Begründung") + planned_amount = models.IntegerField(verbose_name="Erwarteter Betrag", help_text="ganze Euro") + + # Details + invoice_date = models.DateField(blank=True, null=True, verbose_name="Rechnungsdatum") + invoice_number = models.CharField(max_length=20, blank=True, null=True, verbose_name="Rechnungsnummer") + firma = models.CharField(max_length=30, blank=True, null=True, verbose_name="Firma") + amount = models.DecimalField(max_digits=9, decimal_places=2, default=0.00, verbose_name="Betrag") + payout_number = models.IntegerField(blank=True, null=True, verbose_name="Auszahlungsnummer") + + submission_date = models.DateField(blank=True, null=True, verbose_name="Bearbeitungsdatum") + booking_date = models.DateField(default=date.today, verbose_name="Buchungsdatum") + maturity = models.DateField(blank=True, null=True, verbose_name="Fälligkeit") + + upload = models.FileField(upload_to='uploads/fibu/%Y/', default=None, blank=True, null=True, + verbose_name="Scan der Rechnung") + + # Meta information + status = models.IntegerField(default=0, choices=status_choices, verbose_name="Status") + + def get_status(self): + return status_list[self.status] + + def __str__(self): + return "{} ({})".format(self.description, self.account) + + class Meta: + verbose_name = "Buchung" + verbose_name_plural = "Buchungen" + permissions = [ + ('manage_booking', 'Can manage bookings'), + ('request_booking', 'Can request a booking'), + ('check_booking', 'Can check bookings'), + ] diff --git a/schoolapps/fibu/templates/fibu/account/edit.html b/schoolapps/fibu/templates/fibu/account/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..185d5f80fdc5ac53d3c5c824c21717a7f7578e6c --- /dev/null +++ b/schoolapps/fibu/templates/fibu/account/edit.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Buchungskonto bearbeiten</h4> + + <form method="POST" action=""> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_accounts' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> + +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/account/index.html b/schoolapps/fibu/templates/fibu/account/index.html new file mode 100755 index 0000000000000000000000000000000000000000..b12b41f7025d30f4b8f2971cafc268f095351e7d --- /dev/null +++ b/schoolapps/fibu/templates/fibu/account/index.html @@ -0,0 +1,76 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <a class="waves-effect waves-light btn green modal-trigger right" href="#new-modal"> + <i class="material-icons left">add</i> Buchungskonto anlegen + </a> + + <h4>Buchungskonten</h4> + + {% if form.errors %} + <script> + var onFinish = function () { + $("#new-modal").modal("open"); + }; + </script> + {% endif %} + + <form method="POST"> + <div id="new-modal" class="modal"> + <div class="modal-content"> + <h5>Neues Buchungskonto anlegen</h5> + {% csrf_token %} + {% form form=form %} + {% endform %} + </div> + <div class="modal-footer"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Buchungskonto anlegen + </button> + </div> + </div> + </form> + + <table> + <thead> + <tr> + <th>Buchungskonto</th> + <th>Kostenstelle</th> + <th class="right-align">erwartete Einnahmen</th> + <th class="right-align">erwartete Ausgaben</th> + <th>Aktionen</th> + </tr> + </thead> + <tbody> + {% for account in accounts %} + <tr> + <td>{{ account.name }}</td> + <td>{{ account.cost_center }}</td> + {% if account.income %} + <td class="green-text right-align">{{ account.budget }} €</td> + <td></td> + {% else %} + <td></td> + <td class="red-text right-align">{{ account.budget }} €</td> + {% endif %} + <td class="right-align"> + <a href="{% url 'fibu_accounts_edit' account.id %}" + class="waves-effect waves-light btn-flat btn-flat-medium left" title="Bearbeiten"> + <i class="material-icons center green-text">create</i> + </a> + <form action="" method="POST" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ account.id }}" name="id"> + <button type="submit" onclick="return confirm('Buchungskonto wirklich löschen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" title="Löschen"> + <i class="material-icons center red-text">cancel</i> + </button> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/book.html b/schoolapps/fibu/templates/fibu/booking/book.html new file mode 100644 index 0000000000000000000000000000000000000000..0f44c48c719451b596ccf484d9ef90ba84a70b25 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/book.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Buchung bearbeiten</h4> + + <form method="POST"> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_bookings' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/check.html b/schoolapps/fibu/templates/fibu/booking/check.html new file mode 100755 index 0000000000000000000000000000000000000000..9dad97f53214556cb9152fc5093440dc5c13caf5 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/check.html @@ -0,0 +1,44 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Anträge prüfen</h4> + {% if not filter.qs %} + <span class="flow-text center-align"> + Keine offenen Anträge vorhanden + </span> + {% else %} + <div class="collection-item row"> + <div class="col s2"><strong>Antragsteller</strong></div> + <div class="col s3"><strong>Anschaffungswunsch</strong></div> + <div class="col s2 right-align"><strong>Geplante Kosten</strong></div> + <div class="col s3"><strong>Buchungskonto</strong></div> + <div class="col s2"><strong>Aktionen</strong></div> + </div> + + {% for booking in filter.qs %} + <div class="collection-item row"> + <form method="POST"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <div class="col s2">{{ booking.contact.get_full_name }}</div> + <div class="col s3">{{ booking.description }}</div> + <div class="col s2 right-align">{{ booking.planned_amount }} €</div> + <div class="col s3">{{ form.account }}</div> + <div class="col s2"> + <button type="submit" name="allow" + class="waves-effect waves-light btn-flat btn-flat-medium" title="Annehmen"> + <i class="material-icons center green-text">check_circle</i> + </button> + <button type="submit" name="deny" + class="waves-effect waves-light btn-flat btn-flat-medium" title="Ablehnen"> + <i class="material-icons center red-text">not_interested</i> + </button> + </div> + </form> + </div> + {% endfor %} + {% endif %} +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/edit.html b/schoolapps/fibu/templates/fibu/booking/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..4fabb87a6fc09dbb9c0358efd787a0c611337c1f --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/edit.html @@ -0,0 +1,24 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Antrag bearbeiten</h4> + + <form method="POST" action=""> + {% csrf_token %} + {% form form=form %} + {% part form.planned_amount prefix %}<i class="material-icons prefix">euro_symbol</i>{% endpart %} + {% endform %} + + <div class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_index' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </div> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/index.html b/schoolapps/fibu/templates/fibu/booking/index.html new file mode 100755 index 0000000000000000000000000000000000000000..3ad4fd6ebf9f6ae4ae26f842a3d5d285aa7e1381 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/index.html @@ -0,0 +1,68 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <div class="right"> + {% if is_archive %} + <a href="{% url 'fibu_bookings' %}" class="waves-effect waves-light btn grey">Zur aktuellen Übersicht</a> + {% else %} + <a href="{% url 'fibu_bookings_archive' "archive" %}" class="waves-effect waves-light btn grey">Zum Archiv + </a> + {% endif %} + + <a href="{% url 'fibu_bookings_new' %}" class="waves-effect waves-light btn green">Neue Buchung anlegen</a> + </div> + + <h4>{% if is_archive %}Buchungsarchiv{% else %}Aktuelle Buchungen{% endif %}</h4> + + <table> + <thead> + <tr> + <th>Artikelbeschreibung</th> + <th class="right-align">erwarteter Betrag</th> + <th class="right-align">Rechnungsbetrag</th> + <th>Buchungskonto</th> + <th>Antragsteller</th> + <th>Status</th> + <th>Aktionen</th> + </tr> + </thead> + {% for booking in bookings %} + <tr> + <td> + <a href="{% url "fibu_bookings_edit" booking.id %}">{{ booking.description }}</a> + </td> + <td class="right-align">{{ booking.planned_amount }} €</td> + <td class="right-align">{{ booking.amount }} €</td> + <td>{{ booking.account|default:"" }}</td> + <td>{{ booking.contact.get_full_name }}</td> + <td> + <span class="badge new center-align {{ booking.get_status.style_class }}"> + {{ booking.get_status.name }} + </span> + </td> + <td> + <span class="left"> + <a class="waves-effect waves-light btn-flat btn-flat-medium green-text" title="Bearbeiten" + href="{% url "fibu_bookings_edit" booking.id %}"> + <i class="material-icons">edit</i> + </a> + </span> + {% if booking.status < 2 %} + <form method="POST" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <button type="submit" + onclick="return confirm('Wirklich löschen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium red-text" + title="Löschen"> + <i class="material-icons center">cancel</i> + </button> + </form> + {% endif %} + </td> + </tr> + {% endfor %} + </table> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/new.html b/schoolapps/fibu/templates/fibu/booking/new.html new file mode 100644 index 0000000000000000000000000000000000000000..d79cdd2595c1950ee2ec505e0dd8923be165fbdb --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/new.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Neue Buchung</h4> + + <form method="POST"> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Buchung anlegen + </button> + <a href="{% url 'fibu_bookings' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/cost_center/edit.html b/schoolapps/fibu/templates/fibu/cost_center/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..e251f5a3333100c1860bea9c12ce16cb92131f45 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/cost_center/edit.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Kostenstelle bearbeiten</h4> + + <form method="POST"> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_cost_centers' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/cost_center/index.html b/schoolapps/fibu/templates/fibu/cost_center/index.html new file mode 100755 index 0000000000000000000000000000000000000000..87d6b9af09f482dcdf2a5e49de7b341d42076f77 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/cost_center/index.html @@ -0,0 +1,72 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <a class="waves-effect waves-light btn green modal-trigger right" href="#new-modal"> + <i class="material-icons left">add</i> Kostenstelle anlegen + </a> + + <h4>Kostenstellen</h4> + + {% if form.errors %} + <script> + var onFinish = function () { + $("#new-modal").modal("open"); + }; + </script> + {% endif %} + + <form method="POST"> + <div id="new-modal" class="modal"> + <div class="modal-content"> + <h5>Neue Kostenstelle anlegen</h5> + {% csrf_token %} + {% form form=form %} + {% endform %} + </div> + <div class="modal-footer"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Kostenstelle anlegen + </button> + </div> + </div> + </form> + + <table> + <thead> + <tr> + <th>Kostenstelle</th> + <th>Jahr</th> + <th>Aktionen</th> + </tr> + </thead> + {% for cost_center in cost_centers %} + <tr> + <td>{{ cost_center.name }}</td> + <td>{{ cost_center.year }}</td> + <td> + <form action="{% url 'fibu_cost_centers_edit' cost_center.id %}" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ cost_center.id }}" name="id"> + <button type="submit" name="edit" + class="waves-effect waves-light btn-flat btn-flat-medium" title="Bearbeiten"> + <i class="material-icons center green-text">create</i> + </button> + </form> + <form action="" method="POST" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ cost_center.id }}" name="id"> + <button type="submit" + onclick="return confirm('Wollen Sie die Kostenstelle wirklich löschen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" + title="Löschen"> + <i class="material-icons center red-text">cancel</i> + </button> + </form> + </td> + </tr> + {% endfor %} + </table> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/index.html b/schoolapps/fibu/templates/fibu/index.html new file mode 100755 index 0000000000000000000000000000000000000000..3eca9a5d3ba7980a266d30c79d0f0661f0141d15 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/index.html @@ -0,0 +1,108 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <a class="waves-effect waves-light btn green modal-trigger right" href="#new-modal"> + <i class="material-icons left">add</i> Antrag stellen + </a> + <h4>Meine Anträge</h4> + + + {% if form.errors %} + <script> + var onFinish = function () { + $("#new-modal").modal("open"); + }; + </script> + {% endif %} + + <form method="POST"> + <div id="new-modal" class="modal"> + <div class="modal-content"> + <h5>Neuen Antrag stellen</h5> + {% csrf_token %} + {% form form=form %} + {% part form.planned_amount prefix %}<i class="material-icons prefix">euro_symbol</i> + {% endpart %} + {% endform %} + </div> + <div class="modal-footer"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">send</i> Antrag stellen + </button> + </div> + </div> + </form> + +<div class="collection-item row"> + <div class="col s5"><strong>Beschreibung</strong></div> + <div class="col s2 right-align"><strong>erwarteter Betrag</strong></div> + <div class="col s2 right-align"><strong>Rechnungsbetrag</strong></div> + <div class="col s1 center-align"><strong>Status</strong></div> + <div class="col s2 center-align"><strong>Aktionen</strong></div> + </div> + <div class="collection"> + {% for booking in bookings %} + <div class="collection-item row"> + <div class="col s5">{{ booking.description }}</div> + <div class="col s2 right-align">{{ booking.planned_amount }} €</div> + <div class="col s2 right-align">{{ booking.amount }} €</div> + <div class="col s1"> + <span class="badge new {{ booking.get_status.style_class }}">{{ booking.get_status.name }}</span> + </div> + <div class="col s2"> + {# Delete #} + {% if booking.status < 3 %} + <form action="" method="POST" class="right"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <button type="submit" + onclick="return confirm('Wollen Sie den Antrag wirklich zurücknehmen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" + title="Antrag zurücknehmen"> + <i class="material-icons center red-text">cancel</i> + </button> + </form> + {% endif %} + + {# Edit #} + {% if booking.status == 0 %} + <form action="{% url 'fibu_bookings_user_edit' booking.id %}" class="right"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <button type="submit" name="edit" + class="waves-effect waves-light btn-flat btn-flat-medium right" + title="Bearbeiten"> + <i class="material-icons center green-text">create</i> + </button> + </form> + {% endif %} + + <form action="" method="POST" class="right"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <input type="hidden" value="{{ booking.status }}" name="booking-status"> + + + {% if booking.status == 2 %} + <button type="submit" name="ordered" + class="waves-effect waves-light btn-flat btn-flat-medium left" + title="Status auf 'bestellt' ändern"> + <i class="material-icons center green-text">shopping_cart</i> + </button> + + + {% elif booking.status == 3 %} + <button type="submit" name="submit-invoice" + class="waves-effect waves-light btn-flat btn-flat-medium left" + title="Status auf 'Rechnung eingereicht' ändern"> + <i class="material-icons center green-text">description</i> + </button> + {% endif %} + </form> + </div> + </div> + {% endfor %} + </div> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/reports/expenses.html b/schoolapps/fibu/templates/fibu/reports/expenses.html new file mode 100755 index 0000000000000000000000000000000000000000..c35fb8ec1d032c9ad8f1943005f1b0f02d1eda9b --- /dev/null +++ b/schoolapps/fibu/templates/fibu/reports/expenses.html @@ -0,0 +1,36 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Schlussrechnung</h4> + + <div class="collection"> + {% for cost_center, accounts in cost_center_accounts.items %} + <h5 class="collection-item">{{ cost_center }}</h5> + + <div class="collection"> + <div class="row"> + <span class="col s5 m5 white-text grey">Buchungskonto</span> + <span class="col s2 m2 white-text grey right-align">Budget</span> + <span class="col s1 m1 white-text grey right-align">Einnahmen</span> + <span class="col s1 m1 white-text grey right-align">Ausgaben</span> + <span class="col s1 m1 white-text grey right-align">Reste</span> + </div> + {% for account in accounts %} + <div class="collection-item row"> + <span class="col s5 m5">{{ account.name }}</span> + <span class="col s2 m2 right-align">{{ account.budget }} €</span> + <span class="col s1 m1 right-align"> + {% if account.income %}{{ account.saldo }} €{% endif %} + </span> + <span class="col s1 m1 right-align"> + {% if not account.income %}{{ account.saldo }} €{% endif %} + </span> + <span class="col s1 m1 right-align">{{ account.rest }} €</span> + </div> + {% endfor %} + </div> + {% endfor %} + </div> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/reports/index.html b/schoolapps/fibu/templates/fibu/reports/index.html new file mode 100755 index 0000000000000000000000000000000000000000..5399308743095cfc65dfe4b93de840618b9a16e4 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/reports/index.html @@ -0,0 +1,11 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Berichte</h4> + + <div class="collection"> + <a href="{% url "fibu_reports_expenses" %}" class="collection-item">Ausgaben</a> + </div> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/urls.py b/schoolapps/fibu/urls.py new file mode 100755 index 0000000000000000000000000000000000000000..42d8911ac49f9eca2867a53769ce15dc159d033b --- /dev/null +++ b/schoolapps/fibu/urls.py @@ -0,0 +1,19 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path('', views.index, name='fibu_index'), + path('<int:pk>/', views.user_edit, name='fibu_bookings_user_edit'), + path('bookings/check/', views.check, name='fibu_bookings_check'), + path('bookings/new/', views.new_booking, name='fibu_bookings_new'), + path('bookings/', views.booking, name='fibu_bookings'), + path('bookings/<str:is_archive>/', views.booking, name='fibu_bookings_archive'), + path('bookings/<int:pk>/edit/', views.book, name='fibu_bookings_edit'), + path('costcenters/', views.cost_centers, name='fibu_cost_centers'), + path('costcenters/edit/<int:pk>/', views.cost_center_edit, name='fibu_cost_centers_edit'), + path('accounts/', views.account, name='fibu_accounts'), + path('accounts/<int:pk>/', views.account_edit, name='fibu_accounts_edit'), + path('reports/', views.reports, name='fibu_reports'), + path('reports/expenses/', views.expenses, name='fibu_reports_expenses'), +] diff --git a/schoolapps/fibu/views.py b/schoolapps/fibu/views.py new file mode 100644 index 0000000000000000000000000000000000000000..ea3c3afcc38ce7b70949c2d8e5fdfde15ae7b514 --- /dev/null +++ b/schoolapps/fibu/views.py @@ -0,0 +1,266 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.db.models import Sum +from django.urls import reverse +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Booking, CostCenter, Account +from .filters import BookingFilter +from .forms import SimpleBookingForm, CheckBookingForm, CompleteBookingForm, CostCenterForm, AccountForm + + +@login_required +@permission_required('fibu.request_booking') +def index(request): + if request.method == 'POST': + if 'booking-id' in request.POST: + booking_id = request.POST['booking-id'] + booking = get_object_or_404(Booking, pk=booking_id) + + if 'cancel' in request.POST: + booking.delete() + print('Eintrag gelöscht') + return redirect('fibu_index') + + elif 'ordered' in request.POST: + Booking.objects.filter(id=booking_id).update(status=3) + return redirect('fibu_index') + + elif 'submit-invoice' in request.POST: + Booking.objects.filter(id=booking_id).update(status=4) + return redirect('fibu_index') + + form = SimpleBookingForm(instance=booking) + else: + form = SimpleBookingForm(request.POST) + else: + form = SimpleBookingForm() + + if form.is_valid(): + description = form.cleaned_data['description'] + planned_amount = form.cleaned_data['planned_amount'] + justification = form.cleaned_data['justification'] + booking = Booking(description=description, planned_amount=planned_amount, contact=request.user, + justification=justification) + booking.save() + + messages.success(request, "Der Antrag wurde erfolgreich übermittelt.") + + return redirect('fibu_index') + + bookings = Booking.objects.filter(contact=request.user).order_by('status') + + context = {'bookings': bookings, 'form': form} + return render(request, 'fibu/index.html', context) + + +@login_required +@permission_required('fibu.request_booking') +def user_edit(request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = SimpleBookingForm(instance=booking) + + if request.method == 'POST': + form = SimpleBookingForm(request.POST, instance=booking) + + if form.is_valid(): + form.save() + + messages.success(request, "Die Änderungen am Antrag wurden erfolgreich übernommen.") + return redirect(reverse('fibu_index')) + + context = {'form': form} + return render(request, 'fibu/booking/edit.html', context) + + +@login_required +@permission_required('fibu.check_booking') +def check(request): + if request.method == 'POST': + if 'booking-id' in request.POST: + booking_id = request.POST['booking-id'] + if 'allow' in request.POST: + if "account" in request.POST: + account = request.POST['account'] + Booking.objects.filter(id=booking_id).update(status=2, account=account) + messages.success(request, "Der Antrag wurde angenommen.") + else: + messages.error(request, "Bitte wähle ein Buchungskonto aus, um den Antrag anzunehmen.") + elif 'deny' in request.POST: + Booking.objects.filter(id=booking_id).update(status=1) + messages.success(request, "Der Antrag wurde abgelehnt.") + + booking_list = Booking.objects.filter(status=0).order_by('submission_date') + bookings = BookingFilter(request.GET, queryset=booking_list) + form = CheckBookingForm() + return render(request, 'fibu/booking/check.html', {'filter': bookings, 'form': form}) + + +@login_required +@permission_required('fibu.manage_booking') +def booking(request, is_archive=""): + is_archive = is_archive == "archive" + if is_archive: + bookings = Booking.objects.filter(status=5).order_by('-status') + else: + bookings = Booking.objects.filter(status__lt=5).order_by('-status') + context = {'bookings': bookings, 'is_archive': is_archive} + return render(request, 'fibu/booking/index.html', context) + + +@login_required +@permission_required('fibu.manage_booking') +def book(request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = CompleteBookingForm(instance=booking) + template = 'fibu/booking/book.html' + if request.method == 'POST': + form = CompleteBookingForm(request.POST, request.FILES, instance=booking) + if form.is_valid(): + form.save() + messages.success(request, "Die Änderungen an der Buchung wurden erfolgreich übernommen.") + return redirect(reverse('fibu_bookings')) + context = {'form': form} + return render(request, template, context) + + +@login_required +@permission_required('fibu.manage_booking') +def new_booking(request): + form = CompleteBookingForm() + template = 'fibu/booking/new.html' + if request.method == 'POST': + form = CompleteBookingForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request, "Die Buchung wurde erfolgreich angelegt.") + + return redirect(reverse('fibu_bookings')) + context = {'form': form} + return render(request, template, context) + + +@login_required +@permission_required('fibu.manage_costcenter') +def cost_centers(request): + form = CostCenterForm() + + if request.method == 'POST': + if 'id' in request.POST and 'cancel' in request.POST: + cost_center_id = request.POST['id'] + cost_center = CostCenter.objects.get(id=cost_center_id) + cost_center.delete() + + messages.success(request, "Die Kostenstelle wurde erfolgreich gelöscht.") + + return redirect('fibu_cost_centers') + else: + form = CostCenterForm(request.POST) + + if form.is_valid(): + form.save() + messages.success(request, "Die Kostenstelle wurde erfolgreich angelegt.") + return redirect('fibu_cost_centers') + + cost_centers = CostCenter.objects.filter() + + context = {'cost_centers': cost_centers, 'form': form} + return render(request, 'fibu/cost_center/index.html', context) + + +@login_required +@permission_required('fibu.manage_costcenter') +def cost_center_edit(request, pk): + cost_center = get_object_or_404(CostCenter, pk=pk) + form = CostCenterForm(instance=cost_center) + + if request.method == 'POST': + form = CostCenterForm(request.POST, instance=cost_center) + + if form.is_valid(): + form.save() + + messages.success(request, "Die Änderungen an der Kostenstelle wurden erfolgreich übernommen.") + + return redirect(reverse('fibu_cost_centers')) + + context = {'form': form} + return render(request, 'fibu/cost_center/edit.html', context) + + +@login_required +@permission_required('fibu.manage_account') +def account(request): + form = AccountForm() + + if request.method == 'POST': + if 'account-id' in request.POST and 'cancel' in request.POST: + account_id = request.POST['id'] + account = Account.objects.get(id=account_id) + account.delete() + + messages.success(request, "Das Buchungskonto wurde erfolgreich gelöscht") + return redirect('account') + else: + form = AccountForm(request.POST) + + if form.is_valid(): + form.save() + + messages.success(request, "Das Buchungskonto wurde erfolgreich angelegt.") + return redirect('account') + + accounts = Account.objects.filter().order_by('cost_center', '-income', 'name') + context = {'accounts': accounts, 'form': form} + return render(request, 'fibu/account/index.html', context) + + +@login_required +@permission_required('fibu.manage_account') +def account_edit(request, pk): + account = get_object_or_404(Account, pk=pk) + form = AccountForm(instance=account) + + if request.method == 'POST': + form = AccountForm(request.POST, instance=account) + + if form.is_valid(): + form.save() + + messages.success(request, "Die Änderungen am Buchungskonto wurden erfolgreich übernommen.") + return redirect(reverse('account')) + + context = {'form': form} + return render(request, 'fibu/account/edit.html', context) + + +@login_required +@permission_required('fibu.manage_booking') +def reports(request): + return render(request, 'fibu/reports/index.html') + + +@login_required +@permission_required('fibu.manage_booking') +def expenses(request): + cost_centers = CostCenter.objects.filter() + cost_center_accounts = {} + account_rests = {} + for cost_center in cost_centers: + accounts = Account.objects.filter(cost_center=cost_center) + # update saldo + for account in accounts: + saldo = Booking.objects.filter(account=account).aggregate(Sum('amount')) + saldo = saldo['amount__sum'] + try: + rest = account.budget - saldo + except: + rest = account.budget + try: + Account.objects.filter(id=account.id).update(saldo=saldo, rest=rest) + except: + Account.objects.filter(id=account.id).update(saldo=0, rest=account.budget) + + cost_center_accounts[cost_center.name] = list( + Account.objects.filter(cost_center=cost_center).order_by('-income')) + context = {'cost_center_accounts': cost_center_accounts, 'account_rests': account_rests} + return render(request, 'fibu/reports/expenses.html', context) diff --git a/schoolapps/meta.py b/schoolapps/meta.py index de0df08893bff712f1eb06f65cee7cc02d502c9f..9af2f86c59f640d67970a38470a11f3f616d5fbe 100644 --- a/schoolapps/meta.py +++ b/schoolapps/meta.py @@ -10,7 +10,7 @@ with open(copyright_path, "r") as f: COPYRIGHT_SHORT = "© 2018–2019 Mitglieder der Computer-AG, Katharineum zu Lübeck" -VERSION = '1.1.3 "Aebli"' +VERSION = '1.1.4 "Aebli"' LICENSE_APACHE_2 = "Apache 2.0 License" LICENSE_BSD = "2-Clause BSD License" diff --git a/schoolapps/schoolapps/settings.py b/schoolapps/schoolapps/settings.py index 5239dc663f357b6cd3550a7833320ac967c16022..d6a2219b1b0f2cf7cc374e2db8a14020cb85bda5 100755 --- a/schoolapps/schoolapps/settings.py +++ b/schoolapps/schoolapps/settings.py @@ -34,6 +34,7 @@ INSTALLED_APPS = [ 'dashboard.apps.DashboardConfig', "debug.apps.DebugConfig", 'aub.apps.AubConfig', + 'fibu.apps.FibuConfig', 'untisconnect.apps.UntisconnectConfig', 'timetable.apps.TimetableConfig', 'menu.apps.MenuConfig', diff --git a/schoolapps/schoolapps/urls.py b/schoolapps/schoolapps/urls.py index 2e24ef70e17da0a8ab6183c2319ad83b4cd1db07..ba42b84d22a00a0aefe57eb87e08ac9daf6fba37 100755 --- a/schoolapps/schoolapps/urls.py +++ b/schoolapps/schoolapps/urls.py @@ -45,6 +45,11 @@ urlpatterns = [ ####### path('aub/', include('aub.urls')), + ######## + # FIBU # + ######## + path('fibu/', include('fibu.urls')), + ############# # TIMETABLE # ############# diff --git a/schoolapps/static/common/helper.js b/schoolapps/static/common/helper.js index 235e9e61f6c87828a7e7e182cd8c98ced9d81a87..d552d9a659aa5d479f32486800a98d61e50a6972 100644 --- a/schoolapps/static/common/helper.js +++ b/schoolapps/static/common/helper.js @@ -96,10 +96,17 @@ $(document).ready(function () { // Initialize FABs [MAT] $('.fixed-action-btn').floatingActionButton(); + // Initialize Modals [MAT] + $('.modal').modal(); + // Initialize delete button $(".delete-button").click(function (e) { if (!confirm("Wirklich löschen?")) { e.preventDefault(); } - }) + }); + + if (typeof onFinish !== 'undefined') { + onFinish(); + } }); \ No newline at end of file diff --git a/schoolapps/static/common/serviceworker.js b/schoolapps/static/common/serviceworker.js index f91ec73355821fd188ee5248438731058f1c3d32..49e2e2f0960aa588ecdc29bcb22875383c42a9ff 100644 --- a/schoolapps/static/common/serviceworker.js +++ b/schoolapps/static/common/serviceworker.js @@ -1,22 +1,19 @@ -//This is the SchoolApps service worker with Advanced caching +//This is the SchoolApps service worker const CACHE = "schoolapps-cache"; const precacheFiles = [ - // '/', - // '/faq', + '', + '/faq/', ]; const offlineFallbackPage = '/offline'; -const cacheFirstPaths = [ - // '/faq', -]; - const avoidCachingPaths = [ '/admin', '/settings', '/support', + '/tools', '/faq/ask', '/aub/apply_for', '/aub/check1', @@ -44,15 +41,15 @@ function comparePaths(requestUrl, pathsArray) { } self.addEventListener("install", function (event) { - console.log("[SchoolApps PWA] Install Event processing"); + console.log("[SchoolApps PWA] Install Event processing."); - console.log("[SchoolApps PWA] Skip waiting on install"); - self.skipWaiting(); - - event.waitUntil( - caches.open(CACHE).then(function (cache) { - console.log("[SchoolApps PWA] Caching pages during install"); + console.log("[SchoolApps PWA] Skipping waiting on install."); + self.skipWaiting(); + event.waitUntil( + caches.open(CACHE).then(function (cache) { + console.log("[SchoolApps PWA] Caching pages during install."); + return cache.addAll(precacheFiles).then(function () { return cache.add(offlineFallbackPage); }); @@ -62,88 +59,46 @@ self.addEventListener("install", function (event) { // Allow sw to control of current page self.addEventListener("activate", function (event) { - console.log("[SchoolApps PWA] Claiming clients for current page"); - event.waitUntil(self.clients.claim()); + console.log("[SchoolApps PWA] Claiming clients for current page."); + event.waitUntil(self.clients.claim()); }); // If any fetch fails, it will look for the request in the cache and serve it from there first self.addEventListener("fetch", function (event) { - if (event.request.method !== "GET") return; - - if (comparePaths(event.request.url, cacheFirstPaths)) { - cacheFirstFetch(event); - } else { - networkFirstFetch(event); - } + if (event.request.method !== "GET") return; + networkFirstFetch(event); }); -function cacheFirstFetch(event) { - event.respondWith( - fromCache(event.request).then( - function (response) { - // The response was found in the cache so we respond with it and update the entry - - // This is where we call the server to get the newest version of the - // file to use the next time we show view - event.waitUntil( - fetch(event.request).then(function (response) { - return updateCache(event.request, response); - }) - ); - - return response; - }, - function () { - // The response was not found in the cache so we look for it on the server - return fetch(event.request) - .then(function (response) { - // If request was successful, add or update it in the cache - event.waitUntil(updateCache(event.request, response.clone())); - - return response; - }) - .catch(function (error) { - // The following validates that the request was for a navigation to a new document - if (event.request.destination !== "document" || event.request.mode !== "navigate") { - return; - } - - console.log("[SchoolApps PWA] Network request failed and no cache." + error); - // Use the precached offline page as fallback - return caches.match(offlineFallbackPage) - }); - } - ) - ); -} - function networkFirstFetch(event) { - event.respondWith( - fetch(event.request) - .then(function (response) { - // If request was successful, add or update it in the cache - event.waitUntil(updateCache(event.request, response.clone())); - return response; - }) - .catch(function (error) { - console.log("[SchoolApps PWA] Network request failed. Serving content from cache: " + error); - return fromCache(event.request); - }) - ); + event.respondWith( + fetch(event.request) + .then(function (response) { + // If request was successful, add or update it in the cache + console.log("[SchoolApps PWA] Network request successful."); + event.waitUntil(updateCache(event.request, response.clone())); + return response; + }) + .catch(function (error) { + console.log("[SchoolApps PWA] Network request failed. Serving content from cache: " + error); + return fromCache(event); + }) + ); } -function fromCache(request) { - // Check to see if you have it in the cache - // Return response - // If not in the cache, then return offline fallback page - return caches.open(CACHE).then(function (cache) { - return cache.match(request).then(function (matching) { - if (!matching || matching.status === 404) { - return caches.match(offlineFallbackPage); - } - - return matching; - }); +function fromCache(event) { + // Check to see if you have it in the cache + // Return response + // If not in the cache, then return offline fallback page + return caches.open(CACHE).then(function (cache) { + return cache.match(event.request) + .then(function (matching) { + if (!matching || matching.status === 404) { + console.log("[SchoolApps PWA] Cache request failed. Serving offline fallback page."); + // Use the precached offline page as fallback + return caches.match(offlineFallbackPage) + } + + return matching; }); } diff --git a/schoolapps/static/common/style.css b/schoolapps/static/common/style.css index 27506d18004514794e7a7471640541a89613d3ee..2e9d91a3dab402b21ab8401a1f4a76d5af44c0aa 100644 --- a/schoolapps/static/common/style.css +++ b/schoolapps/static/common/style.css @@ -402,7 +402,7 @@ table.striped > tbody > tr:nth-child(odd) { border-color: #b71c1c; } -.alert.primary > p, .alert.primary > div { +.alert.primary > p, .alert.primary > div, .alert.info > p, .alert.info > div { background-color: #ececec; border-color: #da1f3d; } diff --git a/schoolapps/static/common/workbox-sw.js b/schoolapps/static/common/workbox-sw.js deleted file mode 100644 index 61b3289a81a12f15841f55e57ace9063bafa3e51..0000000000000000000000000000000000000000 --- a/schoolapps/static/common/workbox-sw.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}(); -//# sourceMappingURL=workbox-sw.js.map diff --git a/schoolapps/templates/partials/header.html b/schoolapps/templates/partials/header.html index 83d1fa36dfbef936af0ab8f91ccea244f3682c64..a0c320fd86156d421695a267b7582ebd839b7063 100755 --- a/schoolapps/templates/partials/header.html +++ b/schoolapps/templates/partials/header.html @@ -120,7 +120,8 @@ <img src="{% static 'common/logo.png' %}"> </div> <div class="col s6 right-align"> - <a href="/"><strong>SchoolApps</strong></a><br> + <a href="/"><strong>SchoolApps</strong></a> + <br> Katharineum zu Lübeck </div> </div> @@ -147,19 +148,13 @@ <ul class="collapsible collapsible-accordion"> {% if perms.aub.apply_for_aub or perms.aub.check1_aub or perms.aub.check2_aub %} - <li class="bold"> - <a class="collapsible-header waves-effect waves-primary"><i class="material-icons">assignment</i> - Lehrerfunktionen + <li class="bold url-aub_index url-aub_details url-ab_edit url-aub_apply_for urlaub_applied_for"> + <a class="collapsible-header waves-effect waves-primary" href="{% url 'aub_index' %}"><i + class="material-icons">business_center</i> + Unterrichtsbefreiungen </a> <div class="collapsible-body"> <ul> - {% if perms.aub.apply_for_aub %} - <li class="url-aub_index url-aub_details url-ab_edit url-aub_apply_for urlaub_applied_for"> - <a href="{% url 'aub_index' %}"><i class="material-icons">business_center</i> - Unterrichtsbefreiungen - </a> - </li> - {% endif %} {% if perms.aub.check1_aub %} <li class="url-aub_check1"> <a href="{% url 'aub_check1' %}"><i class="material-icons">done</i> Anträge @@ -169,7 +164,8 @@ {% endif %} {% if perms.aub.check2_aub %} <li class="url-aub_check2"> - <a href="{% url 'aub_check2' %}"><i class="material-icons">done_all</i> Anträge + <a href="{% url 'aub_check2' %}"><i class="material-icons">done_all</i> + Anträge genehmigen 2 </a> </li> @@ -189,6 +185,49 @@ <div class="divider"></div> </li> {% endif %} + {% if perms.fibu.request_booking or perms.fibu.manage_booking or perms.fibu.manage_costcenter or perms.fibu.manage.account %} + <li class="bold url-fibu_index url-fibu_bookings_user_edit"> + <a class="collapsible-header waves-effect waves-primary" href="{% url 'fibu_index' %}"><i + class="material-icons">euro_symbol</i> + Finanzen + </a> + <div class="collapsible-body"> + <ul> + {% if perms.fibu.check_booking %} + <li class="url-fibu_bookings_check"> + <a href="{% url 'fibu_bookings_check' %}"><i class="material-icons">done_all</i>Anträge + prüfen + </a> + </li> + {% endif %} + {% if perms.fibu.manage_booking %} + <li class="url-fibu_bookings url-fibu_bookings_edit url-fibu_bookings_new"> + <a href="{% url 'fibu_bookings' %}"><i class="material-icons">receipt</i>Buchungen + </a> + </li> + {% endif %} + {% if perms.fibu.manage_costcenter %} + <li class="url-fibu_cost_centers url-fibu_cost_centers_edit"> + <a href="{% url 'fibu_cost_centers' %}"><i class="material-icons">done</i>Kostenstellen + </a> + </li> + <li class="url-fibu_accounts url-fibu_accounts_edit"> + <a href="{% url 'fibu_accounts' %}"><i class="material-icons">done</i>Buchungskonten + </a> + </li> + {% endif %} + {% if perms.fibu.manage_booking %} + <li class="url-fibu_reports url-fibu_reports_expenses"> + <a href="{% url 'fibu_reports' %}"><i class="material-icons">list</i>Berichte</a> + </li> + {% endif %} + </ul> + </div> + </li> + <li> + <div class="divider"></div> + </li> + {% endif %} {% if perms.timetable.show_plan %} <li class="bold"> @@ -235,11 +274,11 @@ </ul> </div> </li> - {% endif %} - <li> - <div class="divider"></div> - </li> + <li> + <div class="divider"></div> + </li> + {% endif %} <li> <a href="{% url 'menu_show_current' %}" target="_blank"> @@ -247,7 +286,6 @@ </a> </li> - {% if perms.menu.add_menu %} <li class="url-menu_index url-menu_upload url-menu_index_msg"> <a href="{% url 'menu_index' %}"> @@ -359,6 +397,27 @@ {# </p>#} {#</header>#} +{% if messages %} + <header> + {% for message in messages %} + <div class="alert {% if message.tags %}{{ message.tags }}{% else %}info{% endif %}"> + <p> + {% if message.tags == "success" %} + <i class="material-icons left">check_circle</i> + {% elif message.tags == "info" %} + <i class="material-icons left">info</i> + {% elif message.tags == "warning" %} + <i class="material-icons left">warning</i> + {% elif message.tags == "error" %} + <i class="material-icons left">error</i> + {% endif %} + {{ message }} + </p> + </div> + {% endfor %} + </header> +{% endif %} + {% if user.is_authenticated %} <div class="fixed-action-btn"> <a class="btn-floating btn-large green">