diff --git a/analysis/forms.py b/analysis/forms.py new file mode 100644 index 00000000..4aafdcbb --- /dev/null +++ b/analysis/forms.py @@ -0,0 +1,80 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 20.10.21 + +""" +from dal import autocomplete +from django import forms +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID +from konova.forms import BaseForm + + +class TimespanReportForm(BaseForm): + """ TimespanReporForm is used for allowing simple creation of an e.g. annual report for conservation offices + + """ + date_from = forms.DateField( + label_suffix="", + label=_("From"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + date_to = forms.DateField( + label_suffix="", + label=_("To"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + conservation_office = forms.ModelChoiceField( + label=_("Conservation office"), + label_suffix="", + help_text=_("Select the responsible office"), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], + ), + widget=autocomplete.ModelSelect2( + url="codes-conservation-office-autocomplete", + attrs={ + "data-placeholder": _("Click for selection") + } + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Generate report") + self.form_caption = _("Select a timespan and the desired conservation office") + self.action_url = reverse("analysis:reports") + + def save(self) -> str: + """ Generates a redirect url for the detail report + + Returns: + detail_report_url (str): The constructed detail report url + + """ + date_from = self.cleaned_data.get("date_from", None) + date_to = self.cleaned_data.get("date_to", None) + office = self.cleaned_data.get("conservation_office", None) + detail_report_url = reverse("analysis:report-detail", args=(office.id,)) + f"?df={date_from}&dt={date_to}" + return detail_report_url diff --git a/analysis/settings.py b/analysis/settings.py index 5ea5fadf..74978d13 100644 --- a/analysis/settings.py +++ b/analysis/settings.py @@ -6,7 +6,7 @@ Created on: 19.10.21 """ -import datetime - # Defines the date of the legal publishing of the LKompVzVo -LKOMPVZVO_PUBLISH_DATE = datetime.date.fromisoformat("2018-06-16") +from django.utils import timezone + +LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(timezone.datetime.fromisoformat("2018-06-16")) diff --git a/analysis/templates/analysis/reports/detail.html b/analysis/templates/analysis/reports/detail.html index 4497adf3..1ba78d54 100644 --- a/analysis/templates/analysis/reports/detail.html +++ b/analysis/templates/analysis/reports/detail.html @@ -4,10 +4,11 @@ {% block body %}
-

{% trans 'Report' %}

-
{{office.long_name}}
+

{% trans 'Evaluation report' %} {{office.long_name}}

+
{% trans 'From' %} {{report.date_from.date}} {% trans 'to' %} {{report.date_to.date}}
+
{% include 'analysis/reports/includes/intervention/card_intervention.html' %} {% include 'analysis/reports/includes/compensation/card_compensation.html' %} diff --git a/analysis/templates/analysis/reports/index.html b/analysis/templates/analysis/reports/index.html index 4f7d8bd0..9c35d057 100644 --- a/analysis/templates/analysis/reports/index.html +++ b/analysis/templates/analysis/reports/index.html @@ -3,6 +3,8 @@ {% block body %}
-

{% trans 'Reports' %}

+
+ {% include 'form/table/generic_table_form.html' %} +
{% endblock %} \ No newline at end of file diff --git a/analysis/utils/report.py b/analysis/utils/report.py index 4792a166..c9c7a1bd 100644 --- a/analysis/utils/report.py +++ b/analysis/utils/report.py @@ -7,7 +7,6 @@ Created on: 18.10.21 """ from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models.functions import NumGeometries -from django.contrib.gis.measure import Area from django.db.models import Count, Sum, Q from django.db.models.functions import Cast @@ -20,14 +19,18 @@ from konova.models import Geometry class TimespanReport: + """ Holds multiple report elements for a timespan report + + """ office_id = -1 + date_from = -1 + date_to = -1 class InterventionReport: queryset = Intervention.objects.none() queryset_checked = Intervention.objects.none() queryset_recorded = Intervention.objects.none() - # Law related law_sum = -1 law_sum_checked = -1 @@ -45,11 +48,13 @@ class TimespanReport: deduction_sum_checked = -1 deduction_sum_recorded = -1 - def __init__(self, id: str): + def __init__(self, id: str, date_from: str, date_to: str): self.queryset = Intervention.objects.filter( responsible__conservation_office__id=id, legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE, deleted=None, + created__timestamp__gte=date_from, + created__timestamp__lte=date_to, ) self.queryset_checked = self.queryset.filter( checked__isnull=False @@ -160,11 +165,13 @@ class TimespanReport: # Code list id for 'obere Naturschutzbehörde' id_onb = 1943084 - def __init__(self, id: str): + def __init__(self, id: str, date_from: str, date_to: str): self.queryset = Compensation.objects.filter( intervention__responsible__conservation_office__id=id, intervention__legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE, deleted=None, + intervention__created__timestamp__gte=date_from, + intervention__created__timestamp__lte=date_to, ) self.queryset_checked = self.queryset.filter( intervention__checked__isnull=False @@ -282,11 +289,13 @@ class TimespanReport: deductions_sq_m = -1 recorded_deductions_sq_m = -1 - def __init__(self, id: str): + def __init__(self, id: str, date_from: str, date_to: str): # First fetch all eco account for this office self.queryset_total = EcoAccount.objects.filter( responsible__conservation_office__id=id, deleted=None, + created__timestamp__gte=date_from, + created__timestamp__lte=date_to, ) self.queryset_recorded = self.queryset_total.filter( recorded__isnull=False @@ -320,21 +329,23 @@ class TimespanReport: def _evaluate_deductions(self): self.deductions_sq_m = self.queryset_deductions.aggregate( sum=Sum("surface") - )["sum"] + )["sum"] or 0 self.recorded_deductions_sq_m = self.queryset_deductions_recorded.aggregate( sum=Sum("surface") - )["sum"] + )["sum"] or 0 class OldInterventionReport: queryset = Compensation.objects.none() queryset_checked = Compensation.objects.none() queryset_recorded = Compensation.objects.none() - def __init__(self, id: str): + def __init__(self, id: str, date_from: str, date_to: str): self.queryset = Intervention.objects.filter( legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE, responsible__conservation_office__id=id, deleted=None, + created__timestamp__gte=date_from, + created__timestamp__lte=date_to, ) self.queryset_checked = self.queryset.filter( checked__isnull=False @@ -343,9 +354,12 @@ class TimespanReport: recorded__isnull=False ) - def __init__(self, office_id: str): + def __init__(self, office_id: str, date_from: str, date_to: str): self.office_id = office_id - self.intervention_report = self.InterventionReport(self.office_id) - self.compensation_report = self.CompensationReport(self.office_id) - self.eco_account_report = self.EcoAccountReport(self.office_id) - self.old_intervention_report = self.OldInterventionReport(self.office_id) + self.date_from = date_from + self.date_to = date_to + + self.intervention_report = self.InterventionReport(self.office_id, date_from, date_to) + self.compensation_report = self.CompensationReport(self.office_id, date_from, date_to) + self.eco_account_report = self.EcoAccountReport(self.office_id, date_from, date_to) + self.old_intervention_report = self.OldInterventionReport(self.office_id, date_from, date_to) diff --git a/analysis/views.py b/analysis/views.py index 7e297e6a..dab3b276 100644 --- a/analysis/views.py +++ b/analysis/views.py @@ -1,11 +1,16 @@ +from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, redirect +from django.utils import timezone +from analysis.forms import TimespanReportForm from analysis.utils.report import TimespanReport from codelist.models import KonovaCode from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required +from konova.utils.message_templates import FORM_INVALID, PARAMS_INVALID @login_required @@ -20,11 +25,26 @@ def index_reports_view(request: HttpRequest): """ template = "analysis/reports/index.html" - context = {} + form = TimespanReportForm(request.POST or None) + if request.method == "POST": + if form.is_valid(): + redirect_url = form.save() + return redirect(redirect_url) + else: + messages.error( + request, + FORM_INVALID, + extra_tags="danger", + ) + context = { + "form": form + } context = BaseContext(request, context).context return render(request, template, context) +@login_required +@conservation_office_group_required def detail_report_view(request: HttpRequest, id: str): """ Renders the detailed report for a conservation office @@ -35,12 +55,27 @@ def detail_report_view(request: HttpRequest, id: str): Returns: """ - cons_office = get_object_or_404( - KonovaCode, - id=id, - ) - report = TimespanReport(id) + try: + cons_office = KonovaCode.objects.get(id=id) + except (ObjectDoesNotExist, ValueError) as e: + messages.error( + request, + PARAMS_INVALID, + extra_tags="danger", + ) + return redirect("analysis:reports") + try: + date_from = timezone.make_aware(timezone.datetime.fromisoformat(request.GET.get("df", None))) + date_to = timezone.make_aware(timezone.datetime.fromisoformat(request.GET.get("dt", None))) + except ValueError: + messages.error( + request, + PARAMS_INVALID, + extra_tags="danger", + ) + return redirect("analysis:reports") + report = TimespanReport(id, date_from, date_to) template = "analysis/reports/detail.html" context = { "office": cons_office, diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation_views.py index 994e9647..5b187501 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation_views.py @@ -77,7 +77,7 @@ def new_view(request: HttpRequest, intervention_id: str = None): messages.success(request, _("Compensation {} added").format(comp.identifier)) return redirect("compensation:detail", id=comp.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass @@ -132,7 +132,7 @@ def edit_view(request: HttpRequest, id: str): messages.success(request, _("Compensation {} edited").format(comp.identifier)) return redirect("compensation:detail", id=comp.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account_views.py index dadac7e6..bb04facb 100644 --- a/compensation/views/eco_account_views.py +++ b/compensation/views/eco_account_views.py @@ -86,7 +86,7 @@ def new_view(request: HttpRequest): messages.success(request, _("Eco-Account {} added").format(acc.identifier)) return redirect("compensation:acc-detail", id=acc.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass @@ -141,7 +141,7 @@ def edit_view(request: HttpRequest, id: str): messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) return redirect("compensation:acc-detail", id=acc.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass diff --git a/ema/views.py b/ema/views.py index ead1a899..a9ad2bdb 100644 --- a/ema/views.py +++ b/ema/views.py @@ -78,7 +78,7 @@ def new_view(request: HttpRequest): messages.success(request, _("EMA {} added").format(ema.identifier)) return redirect("ema:detail", id=ema.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass @@ -202,7 +202,7 @@ def edit_view(request: HttpRequest, id: str): messages.success(request, _("EMA {} edited").format(ema.identifier)) return redirect("ema:detail", id=ema.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass diff --git a/intervention/views.py b/intervention/views.py index 6853197f..bd770ce2 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -79,7 +79,7 @@ def new_view(request: HttpRequest): messages.success(request, _("Intervention {} added").format(intervention.identifier)) return redirect("intervention:detail", id=intervention.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass @@ -264,7 +264,7 @@ def edit_view(request: HttpRequest, id: str): messages.success(request, _("Intervention {} edited").format(intervention.identifier)) return redirect("intervention:detail", id=intervention.id) else: - messages.error(request, FORM_INVALID) + messages.error(request, FORM_INVALID, extra_tags="danger",) else: # For clarification: nothing in this case pass diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index f9c8e2b9..e28f39de 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _ FORM_INVALID = _("There was an error on this form.") +PARAMS_INVALID = _("Invalid parameters") INTERVENTION_INVALID = _("There are errors in this intervention.") IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier") DATA_UNSHARED = _("This data is not shared with you") diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 5a1733c1..ef92aa2c 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 85533a66..2c6664ec 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -19,7 +19,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-19 16:04+0200\n" +"POT-Creation-Date: 2021-10-20 13:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -29,13 +29,50 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: analysis/forms.py:26 analysis/templates/analysis/reports/detail.html:8 +msgid "From" +msgstr "Vom" + +#: analysis/forms.py:38 +msgid "To" +msgstr "Bis" + +#: analysis/forms.py:49 compensation/forms/forms.py:93 +#: compensation/templates/compensation/detail/eco_account/view.html:58 +#: compensation/templates/compensation/report/eco_account/report.html:16 +#: ema/templates/ema/detail/view.html:42 +#: ema/templates/ema/report/report.html:16 intervention/forms/forms.py:101 +#: intervention/templates/intervention/detail/view.html:56 +#: intervention/templates/intervention/report/report.html:37 +msgid "Conservation office" +msgstr "Eintragungsstelle" + +#: analysis/forms.py:51 compensation/forms/forms.py:95 +msgid "Select the responsible office" +msgstr "Verantwortliche Stelle" + +#: analysis/forms.py:60 compensation/forms/forms.py:67 +#: compensation/forms/forms.py:104 compensation/forms/forms.py:155 +#: intervention/forms/forms.py:63 intervention/forms/forms.py:80 +#: intervention/forms/forms.py:96 intervention/forms/forms.py:112 +msgid "Click for selection" +msgstr "Auswählen..." + +#: analysis/forms.py:67 +msgid "Generate report" +msgstr "Bericht generieren" + +#: analysis/forms.py:68 +msgid "Select a timespan and the desired conservation office" +msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle" + #: analysis/templates/analysis/reports/detail.html:7 -#: compensation/templates/compensation/report/compensation/report.html:7 -#: compensation/templates/compensation/report/eco_account/report.html:7 -#: ema/templates/ema/report/report.html:7 -#: intervention/templates/intervention/report/report.html:7 -msgid "Report" -msgstr "Bericht" +msgid "Evaluation report" +msgstr "Auswertungsbericht" + +#: analysis/templates/analysis/reports/detail.html:8 +msgid "to" +msgstr "bis" #: analysis/templates/analysis/reports/includes/compensation/amount.html:3 #: analysis/templates/analysis/reports/includes/eco_account/amount.html:3 @@ -232,11 +269,6 @@ msgstr "Gesetz" msgid "Old interventions" msgstr "Altfälle" -#: analysis/templates/analysis/reports/index.html:6 -#: templates/navbars/navbar.html:46 -msgid "Reports" -msgstr "Berichte" - #: compensation/filters.py:70 msgid "Show only unrecorded" msgstr "Nur unverzeichnete anzeigen" @@ -288,13 +320,6 @@ msgstr "Förderungen" msgid "Select fundings for this compensation" msgstr "Wählen Sie ggf. Fördermittelprojekte" -#: compensation/forms/forms.py:67 compensation/forms/forms.py:104 -#: compensation/forms/forms.py:155 intervention/forms/forms.py:63 -#: intervention/forms/forms.py:80 intervention/forms/forms.py:96 -#: intervention/forms/forms.py:112 -msgid "Click for selection" -msgstr "Auswählen..." - #: compensation/forms/forms.py:73 compensation/forms/modalForms.py:61 #: compensation/forms/modalForms.py:272 compensation/forms/modalForms.py:367 #: compensation/templates/compensation/detail/compensation/includes/actions.html:34 @@ -318,20 +343,6 @@ msgstr "Kommentar" msgid "Additional comment" msgstr "Zusätzlicher Kommentar" -#: compensation/forms/forms.py:93 -#: compensation/templates/compensation/detail/eco_account/view.html:58 -#: compensation/templates/compensation/report/eco_account/report.html:16 -#: ema/templates/ema/detail/view.html:42 -#: ema/templates/ema/report/report.html:16 intervention/forms/forms.py:101 -#: intervention/templates/intervention/detail/view.html:56 -#: intervention/templates/intervention/report/report.html:37 -msgid "Conservation office" -msgstr "Eintragungsstelle" - -#: compensation/forms/forms.py:95 -msgid "Select the responsible office" -msgstr "Verantwortliche Stelle" - #: compensation/forms/forms.py:109 #: compensation/templates/compensation/detail/eco_account/view.html:62 #: compensation/templates/compensation/report/eco_account/report.html:20 @@ -376,23 +387,23 @@ msgstr "Neue Kompensation" msgid "Edit compensation" msgstr "Bearbeite Kompensation" -#: compensation/forms/forms.py:299 -msgid "New Eco-Account" -msgstr "Neues Ökokonto" - -#: compensation/forms/forms.py:308 -msgid "Eco-Account XY; Location ABC" -msgstr "Ökokonto XY; Flur ABC" - -#: compensation/forms/forms.py:360 +#: compensation/forms/forms.py:290 msgid "Available Surface" msgstr "Verfügbare Fläche" -#: compensation/forms/forms.py:363 +#: compensation/forms/forms.py:293 msgid "The amount that can be used for deductions" msgstr "Die für Abbuchungen zur Verfügung stehende Menge" -#: compensation/forms/forms.py:384 +#: compensation/forms/forms.py:315 +msgid "New Eco-Account" +msgstr "Neues Ökokonto" + +#: compensation/forms/forms.py:324 +msgid "Eco-Account XY; Location ABC" +msgstr "Ökokonto XY; Flur ABC" + +#: compensation/forms/forms.py:377 msgid "Edit Eco-Account" msgstr "Ökokonto bearbeiten" @@ -953,6 +964,13 @@ msgstr "Fehlt" msgid "Action handler" msgstr "Maßnahmenträger" +#: compensation/templates/compensation/report/compensation/report.html:7 +#: compensation/templates/compensation/report/eco_account/report.html:7 +#: ema/templates/ema/report/report.html:7 +#: intervention/templates/intervention/report/report.html:7 +msgid "Report" +msgstr "Bericht" + #: compensation/templates/compensation/report/compensation/report.html:55 #: compensation/templates/compensation/report/eco_account/report.html:72 #: ema/templates/ema/report/report.html:59 @@ -1271,31 +1289,31 @@ msgstr "" "Das Ökokonto {} hat für eine Abbuchung von {} m² nicht ausreichend " "Restfläche. Es stehen noch {} m² zur Verfügung." -#: intervention/models.py:329 +#: intervention/models.py:326 msgid "Registration office file number missing" msgstr "Aktenzeichen Zulassungsbehörde fehlt" -#: intervention/models.py:332 +#: intervention/models.py:329 msgid "Conservation office file number missing" msgstr "Aktenzeichen Naturschutzbehörde fehlt" -#: intervention/models.py:335 +#: intervention/models.py:332 msgid "Responsible data missing" msgstr "Daten zu Verantwortlichen fehlen" -#: intervention/models.py:349 +#: intervention/models.py:346 msgid "Revocation exists" msgstr "Widerspruch liegt vor" -#: intervention/models.py:352 +#: intervention/models.py:349 msgid "Registration date missing" msgstr "Datum Zulassung bzw. Satzungsbeschluss fehlt" -#: intervention/models.py:355 +#: intervention/models.py:352 msgid "Binding on missing" msgstr "Datum Bestandskraft fehlt" -#: intervention/models.py:357 +#: intervention/models.py:354 msgid "Legal data missing" msgstr "Rechtliche Daten fehlen" @@ -1598,10 +1616,14 @@ msgid "There was an error on this form." msgstr "Es gab einen Fehler im Formular." #: konova/utils/message_templates.py:12 +msgid "Invalid parameters" +msgstr "Parameter ungültig" + +#: konova/utils/message_templates.py:13 msgid "There are errors in this intervention." msgstr "Es liegen Fehler in diesem Eingriff vor:" -#: konova/utils/message_templates.py:13 +#: konova/utils/message_templates.py:14 msgid "" "The identifier '{}' had to be changed to '{}' since another entry has been " "added in the meanwhile, which uses this identifier" @@ -1609,11 +1631,11 @@ msgstr "" "Die Kennung '{}' musste zu '{}' geändert werden, da ein anderer Eintrag in " "der Zwischenzeit angelegt wurde, welcher diese Kennung nun bereits verwendet" -#: konova/utils/message_templates.py:14 +#: konova/utils/message_templates.py:15 msgid "This data is not shared with you" msgstr "Diese Daten sind für Sie nicht freigegeben" -#: konova/utils/message_templates.py:15 +#: konova/utils/message_templates.py:16 msgid "You need to be part of another user group." msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!" @@ -1784,6 +1806,10 @@ msgstr "" msgid "Export..." msgstr "" +#: templates/navbars/navbar.html:46 +msgid "Reports" +msgstr "Berichte" + #: templates/navbars/navbar.html:58 user/templates/user/index.html:31 msgid "Settings" msgstr "Einstellungen" @@ -3214,9 +3240,6 @@ msgstr "" #~ msgid "Role changed" #~ msgstr "Rolle geändert" -#~ msgid "Invalid role" -#~ msgstr "Rolle ungültig" - #~ msgid "Official" #~ msgstr "Amtlich"