Merge pull request '26_Annual_conservation_reports' (#33) from 26_Annual_conservation_reports into master

Reviewed-on: SGD-Nord/konova#33
pull/34/head
Michel Peltriaux 3 years ago
commit d47e840dbd

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AnalysisConfig(AppConfig):
name = 'analysis'

@ -0,0 +1,82 @@
"""
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")
self.show_cancel_btn = False
self.action_btn_label = _("Continue")
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

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

@ -0,0 +1,12 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.10.21
"""
# Defines the date of the legal publishing of the LKompVzVo
from django.utils import timezone
LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(timezone.datetime.fromisoformat("2018-06-16"))

@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% load i18n fontawesome_5 %}
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<h3>{% trans 'Evaluation report' %} {{office.long_name}}</h3>
<h5>{% trans 'From' %} {{report.date_from.date}} {% trans 'to' %} {{report.date_to.date}}</h5>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="d-flex justify-content-end">
<div class="dropdown">
<div class="btn btn" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-default" title="{% trans 'Download' %}">
{% fa5_icon 'download' %}
</button>
</div>
<div class="dropdown-menu dropdown-menu-right">
<a href="{{request.url}}?format=excel&{{request.GET.urlencode}}">
<button class="dropdown-item" title="Excel">
{% fa5_icon 'file-excel' %} Excel
</button>
</a>
</div>
</div>
</div>
</div>
</div>
<hr>
<div class="col-sm-12 col-md-12 col-lg-12">
{% include 'analysis/reports/includes/intervention/card_intervention.html' %}
{% include 'analysis/reports/includes/compensation/card_compensation.html' %}
{% include 'analysis/reports/includes/eco_account/card_eco_account.html' %}
{% include 'analysis/reports/includes/old_data/card_old_interventions.html' %}
</div>
{% endblock %}

@ -0,0 +1,55 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Amount' %}</h3>
<strong>
{% blocktrans %}
Checked = Has been checked by the registration office according to LKompVzVo
{% endblocktrans %}
<br>
{% blocktrans %}
Recorded = Has been checked and published by the conservation office
{% endblocktrans %}
</strong>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans 'Area of responsibility' %}</th>
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Number single areas' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans 'Conservation office by law' %}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_unb|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_count|default_if_zero:"-"}}</td>
</tr>
<tr>
<td>{% trans 'Land-use planning' %}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_tbp|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_count|default_if_zero:"-"}}</td>
</tr>
<tr>
<td>{% trans 'Other registration office' %}</td>
<td>{{report.compensation_report.queryset_registration_office_other_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_other|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_count|default_if_zero:"-"}}</td>
</tr>
<tr>
<td><strong>{% trans 'Total' %}</strong></td>
<td><strong>{{report.compensation_report.queryset_checked_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_recorded_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.num_single_surfaces_total|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_count|default_if_zero:"-"}}</strong></td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,23 @@
{% load i18n fontawesome_5 %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div id="compensation" class="card-header cursor-pointer rlp-r" data-toggle="collapse" data-target="#compensationBody" aria-expanded="true" aria-controls="compensationBody">
<div class="row">
<div class="col-sm-6">
<h5>
{% fa5_icon 'leaf' %}
{% trans 'Compensations' %}
</h5>
</div>
</div>
</div>
<div id="compensationBody" class="collapse" aria-labelledby="compensation">
<div class="card-body">
{% include 'analysis/reports/includes/compensation/amount.html' %}
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,24 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Amount' %}</h3>
<strong>
{% blocktrans %}
Recorded = Has been checked and published by the conservation office
{% endblocktrans %}
</strong>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.queryset_count|default_if_zero:"-"}}</td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,25 @@
{% load i18n fontawesome_5 %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div id="ecoAccounts" class="card-header cursor-pointer rlp-r" data-toggle="collapse" data-target="#ecoAccountsBody" aria-expanded="true" aria-controls="ecoAccountsBody">
<div class="row">
<div class="col-sm-6">
<h5>
{% fa5_icon 'tree' %}
{% trans 'Eco-Accounts' %}
</h5>
</div>
</div>
</div>
<div id="ecoAccountsBody" class="collapse" aria-labelledby="ecoAccounts">
<div class="card-body">
{% include 'analysis/reports/includes/eco_account/amount.html' %}
<hr>
{% include 'analysis/reports/includes/eco_account/deductions.html' %}
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,23 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Deductions' %}</h3>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% trans 'Surface' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %} {% trans 'Surface' %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}}m²</td>
<td>{{report.eco_account_report.queryset_deductions_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}m²</td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,30 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Amount' %}</h3>
<strong>
{% blocktrans %}
Checked = Has been checked by the registration office according to LKompVzVo
{% endblocktrans %}
<br>
{% blocktrans %}
Recorded = Has been checked and published by the conservation office
{% endblocktrans %}
</strong>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{report.intervention_report.queryset_checked_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_count|default_if_zero:"-"}}</td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,26 @@
{% load i18n fontawesome_5 %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div id="intervention" class="card-header cursor-pointer rlp-r" data-toggle="collapse" data-target="#interventionBody" aria-expanded="true" aria-controls="interventionBody">
<div class="row">
<div class="col-sm-6">
<h5>
{% fa5_icon 'pencil-ruler' %}
{% trans 'Interventions' %}
</h5>
</div>
</div>
</div>
<div id="interventionBody" class="collapse" aria-labelledby="intervention">
<div class="card-body">
{% include 'analysis/reports/includes/intervention/amount.html' %}
<hr>
{% include 'analysis/reports/includes/intervention/compensated_by.html' %}
<hr>
{% include 'analysis/reports/includes/intervention/laws.html' %}
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,34 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Compensated by' %}</h3>
<div class="table-container scroll-300">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">{% trans 'Compensation type' %}</th>
<th class="w-25" scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th class="w-25" scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th class="w-25" scope="col">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
<tr>
<th>{% trans 'Compensation' %}</th>
<td>{{report.intervention_report.compensation_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum|default_if_zero:"-"}}</td>
</tr>
<tr>
<th>{% trans 'Payment' %}</th>
<td>{{report.intervention_report.payment_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum|default_if_zero:"-"}}</td>
</tr>
<tr>
<th>{% trans 'Deductions' %}</th>
<td>{{report.intervention_report.deduction_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum|default_if_zero:"-"}}</td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,50 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Law usage' %}</h3>
<strong>
{% blocktrans %}
Please note: One intervention can be based on multiple laws. This table therefore does not
count
{% endblocktrans %}
</strong>
<div class="table-container scroll-300">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
{% trans 'Law' %}
</th>
<th scope="col">
{% fa5_icon 'star' %} {% trans 'Checked' %}
</th>
<th scope="col">
{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}
</th>
<th scope="col">
{% trans 'Total' %}
</th>
</tr>
</thead>
<tbody>
{% for law in report.intervention_report.evaluated_laws %}
<tr>
<td>
{{law.short_name}}
<br>
<small>
{{law.long_name}}
</small>
</td>
<td>{{law.num_checked|default_if_zero:"-"}}</td>
<td>{{law.num_recorded|default_if_zero:"-"}}</td>
<td>{{law.num|default_if_zero:"-"}}</td>
</tr>
{% endfor %}
<tr>
<td><strong>{% trans 'Total' %}</strong></td>
<td><strong>{{report.intervention_report.law_sum_checked|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum_recorded|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum|default_if_zero:"-"}}</strong></td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,40 @@
{% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Amount' %}</h3>
<strong>
{% blocktrans %}
Checked = Has been checked by the registration office according to LKompVzVo
{% endblocktrans %}
<br>
{% blocktrans %}
Recorded = Has been checked and published by the conservation office
{% endblocktrans %}
</strong>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="w-25">{% fa5_icon 'star' %} {% trans 'Type' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans 'Intervention' %}</td>
<td>{{report.old_data_report.queryset_intervention_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_intervention_count|default_if_zero:"-"}}</td>
</tr>
<tr>
<td>{% trans 'Compensation' %}</td>
<td>{{report.old_data_report.queryset_comps_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_comps_count|default_if_zero:"-"}}</td>
</tr>
<tr>
<td>{% trans 'Eco-account' %}</td>
<td>{{report.old_data_report.queryset_acc_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_acc_count|default_if_zero:"-"}}</td>
</tr>
</tbody>
</table>
</div>

@ -0,0 +1,24 @@
{% load i18n fontawesome_5 %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div id="oldIntervention" class="card-header cursor-pointer rlp-r" data-toggle="collapse" data-target="#oldInterventionBody" aria-expanded="true" aria-controls="oldInterventionBody">
<div class="row">
<div class="col-sm-6">
<h5>
{% fa5_icon 'pencil-ruler' %}
{% trans 'Old interventions' %}
</h5>
<span>{% trans 'Before' %} 16.06.2018</span>
</div>
</div>
</div>
<div id="oldInterventionBody" class="collapse" aria-labelledby="oldIntervention">
<div class="card-body">
{% include 'analysis/reports/includes/old_data/amount.html' %}
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load i18n fontawesome_5 %}
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
{% include 'form/table/generic_table_form.html' %}
</div>
</div>
{% endblock %}

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,15 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.10.21
"""
from django.urls import path
from analysis.views import *
app_name = "analysis"
urlpatterns = [
path("reports/", index_reports_view, name="reports"),
path("reports/<id>", detail_report_view, name="report-detail"),
]

@ -0,0 +1,114 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 21.10.21
"""
from django.core.files.temp import NamedTemporaryFile
from openpyxl import load_workbook
class TempExcelFile:
""" A temporary excel sheet which will not be saved on the hard drive permanently.
Using a template file and a template_map dictionary, this class can be used to fill in automatically
predefined values into certain cells.
Can be used to create excel files from data and sending it as a response like
_file = TempExcelFile()
response = HttpResponse(
content=file.stream,
content_type="application/ms-excel",
)
response['Content-Disposition'] = 'attachment; filename=my_file.xlsx'
return response
"""
stream = None
_template_file_path = None
_template_map = {}
_data_obj = None
def __init__(self, template_file_path: str = None, template_map: dict = None):
self._template_map = template_map or {}
self._template_file_path = template_file_path
self._workbook = load_workbook(template_file_path)
self._file = NamedTemporaryFile()
self._replace_template_placeholders()
def _replace_template_placeholders(self, start_row: int = 0):
""" Replaces all placeholder inside the template according to _template_map
Args:
start_row (int): Defines where to start
Returns:
"""
sheets = self._workbook.worksheets
for sheet in sheets:
ws = sheet
# Always activate sheet protection
ws.protection.sheet = True
ws.protection.enable()
_rows = ws.iter_rows(start_row)
for row in _rows:
for cell in row:
val = cell.value
if val in self._template_map:
attr = self._template_map[val]
# If keyword '_iter' can be found inside the placeholder value it's an iterable and we
# need to process it differently
if isinstance(attr, dict):
# Read the iterable object and related attributes from the dict
_iter_obj = attr.get("iterable", None)
_attrs = attr.get("attrs", [])
self._add_cells_from_iterable(ws, cell, _iter_obj, _attrs)
# Since the sheet length did change now, we need to rerun this function starting with the new
# row counter
self._replace_template_placeholders(start_row=cell.row + len(_iter_obj))
else:
cell.value = attr
self._workbook.save(self._file.name)
self._file.seek(0)
self.stream = self._file.read()
def _add_cells_from_iterable(self, ws, start_cell, _iter_obj: iter, _attrs: list):
"""
Adds iterable data defined by _template_map like
...
"some_placeholder_iter": {
"iterable": iterable_object,
"attrs": [
"attr1",
"attr2",
"attr3",
...
]
},
...
Args:
ws (Workbook): The active workbook
_iter_obj (dict): Iterable definitions from template_map
Returns:
"""
# Save border style
border_style = start_cell.border.copy()
# Drop current row, since it is just placeholder
ws.delete_rows(start_cell.row)
# Add enoug empty rows for the data
ws.insert_rows(start_cell.row, len(_iter_obj))
i = 0
for _iter_entry in _iter_obj:
j = 0
for _iter_attr in _attrs:
_new_cell = ws.cell(start_cell.row + i, start_cell.column + j, getattr(_iter_entry, _iter_attr))
_new_cell.border = border_style
j += 1
i += 1

@ -0,0 +1,547 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 18.10.21
"""
from django.contrib.gis.db.models import MultiPolygonField
from django.contrib.gis.db.models.functions import NumGeometries
from django.db.models import Count, Sum, Q
from django.db.models.functions import Cast
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from codelist.models import KonovaCode
from codelist.settings import CODELIST_LAW_ID
from compensation.models import Compensation, Payment, EcoAccountDeduction, EcoAccount
from intervention.models import Intervention
from konova.models import Geometry
from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT
class TimespanReport:
""" Holds multiple report elements for a timespan report
"""
office_id = -1
date_from = -1
date_to = -1
# Excel map is used to map a cell value ("A1") to an attribute
excel_map = {}
excel_template_path = f"{BASE_DIR}/analysis/utils/excel/excel_report.xlsx"
class InterventionReport:
queryset = Intervention.objects.none()
queryset_checked = Intervention.objects.none()
queryset_recorded = Intervention.objects.none()
queryset_count = -1
queryset_checked_count = -1
queryset_recorded_count = -1
# Law related
law_sum = -1
law_sum_checked = -1
law_sum_recorded = -1
evaluated_laws = None
# Compensations related
compensation_sum = -1
compensation_sum_checked = -1
compensation_sum_recorded = -1
payment_sum = -1
payment_sum_checked = -1
payment_sum_recorded = -1
deduction_sum = -1
deduction_sum_checked = -1
deduction_sum_recorded = -1
excel_map = {}
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
)
self.queryset_recorded = self.queryset.filter(
recorded__isnull=False
)
self.queryset_count = self.queryset.count()
self.queryset_checked_count = self.queryset_checked.count()
self.queryset_recorded_count = self.queryset_recorded.count()
self._create_report()
self._define_excel_map()
def _define_excel_map(self):
""" Define the excel map, which holds values for each placeholder used in the template
Returns:
"""
self.excel_map = {
"i_checked": self.queryset_checked_count,
"i_recorded": self.queryset_recorded_count,
"i_total": self.queryset_count,
"i_compensations_checked": self.compensation_sum_checked,
"i_compensations_recorded": self.compensation_sum_recorded,
"i_compensations_total": self.compensation_sum,
"i_payments_recorded": self.payment_sum_recorded,
"i_payments_checked": self.payment_sum_checked,
"i_payments_total": self.payment_sum,
"i_deductions_recorded": self.deduction_sum_recorded,
"i_deductions_checked": self.deduction_sum_checked,
"i_deductions_total": self.deduction_sum,
"i_laws_iter": {
"iterable": self.evaluated_laws,
"attrs": [
"short_name",
"num_checked",
"num_recorded",
"num",
]
},
"i_laws_checked": self.law_sum_checked,
"i_laws_recorded": self.law_sum_recorded,
"i_laws_total": self.law_sum,
}
def _create_report(self):
""" Creates all report information
Returns:
"""
self._evaluate_laws()
self._evaluate_compensations()
def _evaluate_laws(self):
""" Analyzes the intervention-law distribution
Returns:
"""
# Count interventions based on law
# Fetch all KonovaCodes for laws, sorted alphabetically
laws = KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_LAW_ID],
).order_by(
"long_name"
)
# Fetch all law ids which are used by any .legal object of an intervention object
intervention_laws_total = self.queryset.values_list("legal__laws__id")
intervention_laws_checked = self.queryset.filter(checked__isnull=False).values_list("legal__laws__id")
intervention_laws_recorded = self.queryset.filter(recorded__isnull=False).values_list(
"legal__laws__id")
# Count how often which law id appears in the above list, return only the long_name of the law and the resulting
# count (here 'num'). This is for keeping the db fetch as small as possible
# Compute the sum for total, checked and recorded
self.evaluated_laws = laws.annotate(
num=Count("id", filter=Q(id__in=intervention_laws_total)),
num_checked=Count("id", filter=Q(id__in=intervention_laws_checked)),
num_recorded=Count("id", filter=Q(id__in=intervention_laws_recorded)),
).values_list("short_name", "long_name", "num_checked", "num_recorded", "num", named=True)
self.law_sum = self.evaluated_laws.aggregate(sum_num=Sum("num"))["sum_num"]
self.law_sum_checked = self.evaluated_laws.aggregate(sum_num_checked=Sum("num_checked"))["sum_num_checked"]
self.law_sum_recorded = self.evaluated_laws.aggregate(sum_num_recorded=Sum("num_recorded"))["sum_num_recorded"]
def _evaluate_compensations(self):
""" Analyzes the types of compensation distribution
Returns:
"""
# Count all compensations
comps = Compensation.objects.filter(
intervention__in=self.queryset
)
self.compensation_sum = comps.count()
self.compensation_sum_checked = comps.filter(intervention__checked__isnull=False).count()
self.compensation_sum_recorded = comps.filter(intervention__recorded__isnull=False).count()
# Count all payments
payments = Payment.objects.filter(
intervention__in=self.queryset
)
self.payment_sum = payments.count()
self.payment_sum_checked = payments.filter(intervention__checked__isnull=False).count()
self.payment_sum_recorded = payments.filter(intervention__recorded__isnull=False).count()
# Count all deductions
deductions = EcoAccountDeduction.objects.filter(
intervention__in=self.queryset
)
self.deduction_sum = deductions.count()
self.deduction_sum_checked = deductions.filter(intervention__checked__isnull=False).count()
self.deduction_sum_recorded = deductions.filter(intervention__recorded__isnull=False).count()
class CompensationReport:
queryset = Compensation.objects.none()
queryset_checked = Compensation.objects.none()
queryset_recorded = Compensation.objects.none()
queryset_count = -1
queryset_checked_count = -1
queryset_recorded_count = -1
queryset_registration_office_unb = Compensation.objects.none()
queryset_registration_office_unb_checked = Compensation.objects.none()
queryset_registration_office_unb_recorded = Compensation.objects.none()
queryset_registration_office_unb_count = -1
queryset_registration_office_unb_checked_count = -1
queryset_registration_office_unb_recorded_count = -1
num_single_surfaces_total_unb = -1
queryset_registration_office_tbp = Compensation.objects.none()
queryset_registration_office_tbp_checked = Compensation.objects.none()
queryset_registration_office_tbp_recorded = Compensation.objects.none()
queryset_registration_office_tbp_count = -1
queryset_registration_office_tbp_checked_count = -1
queryset_registration_office_tbp_recorded_count = -1
num_single_surfaces_total_tbp = -1
queryset_registration_office_other = Compensation.objects.none()
queryset_registration_office_other_checked = Compensation.objects.none()
queryset_registration_office_other_recorded = Compensation.objects.none()
queryset_registration_office_other_count = -1
queryset_registration_office_other_checked_count = -1
queryset_registration_office_other_recorded_count = -1
num_single_surfaces_total_other = -1
num_single_surfaces_total = -1
num_single_surfaces_recorded = -1
# Code list id for 'Träger der Bauleitplanung' parent
id_tbp = 1943695
# Code list id for 'untere Naturschutzbehörde'
id_unb = 1943087
# Code list id for 'obere Naturschutzbehörde'
id_onb = 1943084
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
)
self.queryset_recorded = self.queryset.filter(
intervention__recorded__isnull=False
)
self.queryset_count = self.queryset.count()
self.queryset_checked_count = self.queryset_checked.count()
self.queryset_recorded_count = self.queryset_recorded.count()
self._create_report()
self._define_excel_map()
def _define_excel_map(self):
""" Define the excel map, which holds values for each placeholder used in the template
Returns:
"""
self.excel_map = {
"c_unb_checked": self.queryset_registration_office_unb_checked_count,
"c_unb_recorded": self.queryset_registration_office_unb_recorded_count,
"c_unb": self.queryset_registration_office_unb_count,
"c_surfaces_unb": self.num_single_surfaces_total_unb,
"c_tbp_checked": self.queryset_registration_office_tbp_checked_count,
"c_tbp_recorded": self.queryset_registration_office_tbp_recorded_count,
"c_tbp": self.queryset_registration_office_tbp_count,
"c_surfaces_tbp": self.num_single_surfaces_total_tbp,
"c_other_checked": self.queryset_registration_office_other_checked_count,
"c_other_recorded": self.queryset_registration_office_other_recorded_count,
"c_other": self.queryset_registration_office_other_count,
"c_surfaces_other": self.num_single_surfaces_total_other,
"c_checked": self.queryset_checked_count,
"c_recorded": self.queryset_recorded_count,
"c_total": self.queryset_count,
"c_surfaces": self.num_single_surfaces_total,
}
def _create_report(self):
""" Creates all report information
Returns:
"""
self._evaluate_compensation_responsibility()
self._evaluate_surfaces()
def _evaluate_surfaces(self):
""" Evaluates the surfaces of compensation Multipolygon fields
Returns:
"""
# Evaluate all surfaces
ids = self.queryset.values_list("geometry_id")
self.num_single_surfaces_total = self._count_geometry_surfaces(ids)
# Evaluate surfaces where the conservation office is the registration office as well
ids = self.queryset_registration_office_unb.values_list("geometry_id")
self.num_single_surfaces_total_unb = self._count_geometry_surfaces(ids)
# Evaluates surfaces where the registration office is a Träger Bauleitplanung
ids = self.queryset_registration_office_tbp.values_list("geometry_id")
self.num_single_surfaces_total_tbp = self._count_geometry_surfaces(ids)
# Evaluates surfaces where any other registration office is responsible
ids = self.queryset_registration_office_other.values_list("geometry_id")
self.num_single_surfaces_total_other = self._count_geometry_surfaces(ids)
def _count_geometry_surfaces(self, ids: list):
""" Wraps counting of geometry surfaces from a given list of ids
Args:
ids (list): List of geometry ids
Returns:
"""
# Now select all geometries matching the ids
# Then perform a ST_NumGeometries variant over all geometries
# Then sum up all of the calculated surface numbers
return Geometry.objects.filter(
id__in=ids
).annotate(
geom_cast=Cast("geom", MultiPolygonField())
).annotate(
num=NumGeometries("geom_cast")
).aggregate(
num_geoms=Sum("num")
)["num_geoms"] or 0
def _evaluate_compensation_responsibility(self):
""" Evaluates compensations based on different responsibility areas
unb -> Untere Naturschutzbehörde
Holds entries where conservation_office and registration_office basically are the same
tbp -> Träger Bauleitplanung
Holds entries where registration_office is a Träger der Bauleitplanung
other -> Other registration offices
Holds all other entries
Returns:
"""
self.queryset_registration_office_unb = self.queryset.filter(
intervention__responsible__registration_office__parent__id=self.id_unb
)
self.queryset_registration_office_unb_recorded = self.queryset_registration_office_unb.filter(
intervention__recorded__isnull=False,
)
self.queryset_registration_office_unb_checked = self.queryset_registration_office_unb.filter(
intervention__checked__isnull=False,
)
self.queryset_registration_office_unb_count = self.queryset_registration_office_unb.count()
self.queryset_registration_office_unb_checked_count = self.queryset_registration_office_unb_checked.count()
self.queryset_registration_office_unb_recorded_count = self.queryset_registration_office_unb_recorded.count()
self.queryset_registration_office_tbp = self.queryset.filter(
intervention__responsible__registration_office__parent__id=self.id_tbp
)
self.queryset_registration_office_tbp_recorded = self.queryset_registration_office_tbp.filter(
intervention__recorded__isnull=False,
)
self.queryset_registration_office_tbp_checked = self.queryset_registration_office_tbp.filter(
intervention__checked__isnull=False,
)
self.queryset_registration_office_tbp_count = self.queryset_registration_office_tbp.count()
self.queryset_registration_office_tbp_checked_count = self.queryset_registration_office_tbp_checked.count()
self.queryset_registration_office_tbp_recorded_count = self.queryset_registration_office_tbp_recorded.count()
self.queryset_registration_office_other = self.queryset.exclude(
Q(id__in=self.queryset_registration_office_tbp) | Q(id__in=self.queryset_registration_office_unb)
)
self.queryset_registration_office_other_recorded = self.queryset_registration_office_other.filter(
intervention__recorded__isnull=False,
)
self.queryset_registration_office_other_checked = self.queryset_registration_office_other.filter(
intervention__checked__isnull=False,
)
self.queryset_registration_office_other_count = self.queryset_registration_office_other.count()
self.queryset_registration_office_other_checked_count = self.queryset_registration_office_other_checked.count()
self.queryset_registration_office_other_recorded_count = self.queryset_registration_office_other_recorded.count()
class EcoAccountReport:
queryset = EcoAccount.objects.none()
queryset_recorded = EcoAccount.objects.none()
queryset_count = -1
queryset_recorded_count = -1
queryset_deductions = EcoAccountDeduction.objects.none()
queryset_deductions_recorded = EcoAccountDeduction.objects.none()
queryset_has_deductions = EcoAccountDeduction.objects.none()
queryset_deductions_count = -1
queryset_deductions_recorded_count = -1
queryset_has_deductions_count = -1
# Total size of deductions
deductions_sq_m = -1
recorded_deductions_sq_m = -1
def __init__(self, id: str, date_from: str, date_to: str):
# First fetch all eco account for this office
self.queryset = 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.filter(
recorded__isnull=False
)
# Fetch all related deductions
self.queryset_deductions = EcoAccountDeduction.objects.filter(
account__id__in=self.queryset.values_list("id")
)
# Fetch deductions for interventions which are already recorded
self.queryset_deductions_recorded = self.queryset_deductions.filter(
intervention__recorded__isnull=False
)
self.queryset_count = self.queryset.count()
self.queryset_recorded_count = self.queryset_recorded.count()
self.queryset_deductions_count = self.queryset_deductions.count()
self.queryset_deductions_recorded_count = self.queryset_deductions_recorded.count()
self.queryset_has_deductions_count = self.queryset_has_deductions.count()
self._create_report()
self._define_excel_map()
def _define_excel_map(self):
""" Define the excel map, which holds values for each placeholder used in the template
Returns:
"""
self.excel_map = {
"acc_total": self.queryset_count,
"acc_recorded": self.queryset_recorded_count,
"acc_deduc_recorded": self.queryset_deductions_recorded_count,
"acc_deduc_surface_recorded": self.recorded_deductions_sq_m,
"acc_deduc_total": self.queryset_deductions_count,
"acc_deduc_surface_total": self.deductions_sq_m,
}
def _create_report(self):
""" Creates all report information
Returns:
"""
self._evaluate_deductions()
def _evaluate_deductions(self):
self.deductions_sq_m = self.queryset_deductions.aggregate(
sum=Sum("surface")
)["sum"] or 0
self.recorded_deductions_sq_m = self.queryset_deductions_recorded.aggregate(
sum=Sum("surface")
)["sum"] or 0
class OldDataReport:
"""
Evaluates 'old data' (registered (zugelassen) before 16.06.2018)
"""
queryset_intervention = Intervention.objects.none()
queryset_intervention_recorded = Intervention.objects.none()
queryset_intervention_count = -1
queryset_intervention_recorded_count = -1
queryset_comps = Compensation.objects.none()
queryset_comps_recorded = Compensation.objects.none()
queryset_comps_count = -1
queryset_comps_recorded_count = -1
queryset_acc = EcoAccount.objects.none()
queryset_acc_recorded = EcoAccount.objects.none()
queryset_acc_count = -1
queryset_acc_recorded_count = -1
def __init__(self, id: str, date_from: str, date_to: str):
self.queryset_intervention = 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_intervention_recorded = self.queryset_intervention.filter(
recorded__isnull=False
)
self.queryset_intervention_count = self.queryset_intervention.count()
self.queryset_intervention_recorded_count = self.queryset_intervention_recorded.count()
self.queryset_comps = Compensation.objects.filter(
intervention__in=self.queryset_intervention
)
self.queryset_comps_recorded = Compensation.objects.filter(
intervention__in=self.queryset_intervention_recorded,
)
self.queryset_comps_count = self.queryset_comps.count()
self.queryset_comps_recorded_count = self.queryset_comps_recorded.count()
self.queryset_acc = EcoAccount.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_acc_recorded = self.queryset_acc.filter(
recorded__isnull=False,
)
self.queryset_acc_count = self.queryset_acc.count()
self.queryset_acc_recorded_count = self.queryset_acc_recorded.count()
self._define_excel_map()
def _define_excel_map(self):
""" Define the excel map, which holds values for each placeholder used in the template
Returns:
"""
self.excel_map = {
"old_i_recorded": self.queryset_intervention_recorded_count,
"old_i_total": self.queryset_intervention_count,
"old_c_recorded": self.queryset_comps_recorded_count,
"old_c_total": self.queryset_comps_count,
"old_ea_recorded": self.queryset_acc_recorded_count,
"old_ea_total": self.queryset_acc_count,
}
def __init__(self, office_id: str, date_from: str, date_to: str):
self.office_id = 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_data_report = self.OldDataReport(self.office_id, date_from, date_to)
# Build excel map
self.excel_map = {
"date_from": date_from.strftime(DEFAULT_DATE_FORMAT),
"date_to": date_to.strftime(DEFAULT_DATE_FORMAT),
}
self.excel_map.update(self.intervention_report.excel_map)
self.excel_map.update(self.compensation_report.excel_map)
self.excel_map.update(self.eco_account_report.excel_map)
self.excel_map.update(self.old_data_report.excel_map)

@ -0,0 +1,98 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from analysis.forms import TimespanReportForm
from analysis.utils.excel.excel import TempExcelFile
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
@conservation_office_group_required
def index_reports_view(request: HttpRequest):
"""
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "analysis/reports/index.html"
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
Args:
request (HttpRequest): The incoming request
id (str): The conservation_office KonovaCode id
Returns:
"""
# Try to resolve the requested office id
cons_office = get_object_or_404(
KonovaCode,
id=id
)
# Try to resolve the date parameters into Date objects -> redirect if this fails
try:
df = request.GET.get("df", None)
dt = request.GET.get("dt", None)
date_from = timezone.make_aware(timezone.datetime.fromisoformat(df))
date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt))
except ValueError:
messages.error(
request,
PARAMS_INVALID,
extra_tags="danger",
)
return redirect("analysis:reports")
# Check whether the html default rendering is requested or an alternative
format_param = request.GET.get("format", "html")
report = TimespanReport(id, date_from, date_to)
if format_param == "html":
template = "analysis/reports/detail.html"
context = {
"office": cons_office,
"report": report,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif format_param == "excel":
file = TempExcelFile(report.excel_template_path, report.excel_map)
response = HttpResponse(
content=file.stream,
content_type="application/ms-excel",
)
response['Content-Disposition'] = f'attachment; filename={cons_office.long_name}_{df}_{dt}.xlsx'
return response
else:
raise NotImplementedError

@ -16,7 +16,7 @@ from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_FUNDING_ID, CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_COMPENSATION_FUNDING_ID, CODELIST_CONSERVATION_OFFICE_ID
from compensation.models import Compensation, EcoAccount from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput from intervention.inputs import GenerateInput
from intervention.models import Intervention, ResponsibilityData from intervention.models import Intervention, ResponsibilityData, LegalData
from konova.forms import BaseForm, SimpleGeomForm from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
@ -284,10 +284,40 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
""" """
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [ field_order = [
"identifier", "identifier",
"title", "title",
"conservation_office", "conservation_office",
"registration_date",
"surface",
"conservation_file_number", "conservation_file_number",
"handler", "handler",
"fundings", "fundings",
@ -313,7 +343,9 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
identifier = self.cleaned_data.get("identifier", None) identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None) fundings = self.cleaned_data.get("fundings", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler = self.cleaned_data.get("handler", None) handler = self.cleaned_data.get("handler", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None) conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
@ -332,15 +364,20 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
conservation_office=conservation_office, conservation_office=conservation_office,
) )
legal = LegalData.objects.create(
registration_date=registration_date
)
# Finally create main object # Finally create main object
acc = EcoAccount.objects.create( acc = EcoAccount.objects.create(
identifier=identifier, identifier=identifier,
title=title, title=title,
responsible=responsible, responsible=responsible,
deductable_surface=0.00, deductable_surface=surface,
created=action, created=action,
geometry=geometry, geometry=geometry,
comment=comment, comment=comment,
legal=legal
) )
acc.fundings.set(fundings) acc.fundings.set(fundings)
acc.users.add(user) acc.users.add(user)
@ -354,30 +391,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts """ Form for editing eco accounts
""" """
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"surface",
"conservation_file_number",
"handler",
"fundings",
"comment",
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -387,11 +400,15 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.cancel_redirect = reverse("compensation:acc-detail", args=(self.instance.id,)) self.cancel_redirect = reverse("compensation:acc-detail", args=(self.instance.id,))
# Initialize form data # Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = { form_data = {
"identifier": self.instance.identifier, "identifier": self.instance.identifier,
"title": self.instance.title, "title": self.instance.title,
"surface": self.instance.deductable_surface, "surface": self.instance.deductable_surface,
"handler": self.instance.responsible.handler, "handler": self.instance.responsible.handler,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office, "conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number, "conservation_file_number": self.instance.responsible.conservation_file_number,
"fundings": self.instance.fundings.all(), "fundings": self.instance.fundings.all(),
@ -409,6 +426,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
identifier = self.cleaned_data.get("identifier", None) identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None) fundings = self.cleaned_data.get("fundings", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler = self.cleaned_data.get("handler", None) handler = self.cleaned_data.get("handler", None)
surface = self.cleaned_data.get("surface", None) surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
@ -429,6 +447,10 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.responsible.conservation_file_number = conservation_file_number self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save() self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier self.instance.identifier = identifier
self.instance.title = title self.instance.title = title

@ -19,7 +19,7 @@ from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES
CODELIST_COMPENSATION_FUNDING_ID CODELIST_COMPENSATION_FUNDING_ID
from compensation.managers import CompensationStateManager, EcoAccountDeductionManager, CompensationActionManager, \ from compensation.managers import CompensationStateManager, EcoAccountDeductionManager, CompensationActionManager, \
EcoAccountManager, CompensationManager EcoAccountManager, CompensationManager
from intervention.models import Intervention, ResponsibilityData from intervention.models import Intervention, ResponsibilityData, LegalData
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \ from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
generate_document_file_upload_path generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
@ -309,6 +309,14 @@ class EcoAccount(AbstractCompensation):
default=0, default=0,
) )
legal = models.OneToOneField(
LegalData,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on legal dates or law"
)
objects = EcoAccountManager() objects = EcoAccountManager()
def __str__(self): def __str__(self):

@ -61,6 +61,10 @@
<th scope="row">{% trans 'Conservation office file number' %}</th> <th scope="row">{% trans 'Conservation office file number' %}</th>
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
</tr> </tr>
<tr {% if not obj.legal.registration_date %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
<th scope="row">{% trans 'Agreement date' %}</th>
<td class="align-middle">{{obj.legal.registration_date|default_if_none:""}}</td>
</tr>
<tr {% if not obj.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not obj.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
<th scope="row">{% trans 'Action handler' %}</th> <th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>

@ -77,7 +77,7 @@ def new_view(request: HttpRequest, intervention_id: str = None):
messages.success(request, _("Compensation {} added").format(comp.identifier)) messages.success(request, _("Compensation {} added").format(comp.identifier))
return redirect("compensation:detail", id=comp.id) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass
@ -132,7 +132,7 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("Compensation {} edited").format(comp.identifier)) messages.success(request, _("Compensation {} edited").format(comp.identifier))
return redirect("compensation:detail", id=comp.id) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass

@ -86,7 +86,7 @@ def new_view(request: HttpRequest):
messages.success(request, _("Eco-Account {} added").format(acc.identifier)) messages.success(request, _("Eco-Account {} added").format(acc.identifier))
return redirect("compensation:acc-detail", id=acc.id) return redirect("compensation:acc-detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass
@ -141,7 +141,7 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
return redirect("compensation:acc-detail", id=acc.id) return redirect("compensation:acc-detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass

@ -78,7 +78,7 @@ def new_view(request: HttpRequest):
messages.success(request, _("EMA {} added").format(ema.identifier)) messages.success(request, _("EMA {} added").format(ema.identifier))
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass
@ -202,7 +202,7 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("EMA {} edited").format(ema.identifier)) messages.success(request, _("EMA {} edited").format(ema.identifier))
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass

@ -23,26 +23,3 @@ class InterventionManager(models.Manager):
).prefetch_related( ).prefetch_related(
"users", "users",
) )
class LegalDataManager(models.Manager):
""" Holds default db fetch setting for this model type
"""
def get_queryset(self):
return super().get_querset().select_related(
"process_type",
).prefetch_related(
"laws"
)
class ResponsibilityDataManager(models.Manager):
""" Holds default db fetch setting for this model type
"""
def get_queryset(self):
return super().get_querset().select_related(
"registration_office",
"conservation_office",
)

@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \ from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \
CODELIST_PROCESS_TYPE_ID CODELIST_PROCESS_TYPE_ID
from intervention.managers import InterventionManager, LegalDataManager, ResponsibilityDataManager from intervention.managers import InterventionManager
from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \ from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \
generate_document_file_upload_path generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT
@ -56,7 +56,6 @@ class ResponsibilityData(UuidModel):
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True) conservation_file_number = models.CharField(max_length=1000, blank=True, null=True)
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'") handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'")
objects = ResponsibilityDataManager()
def __str__(self): def __str__(self):
return "ZB: {} | ETS: {} | Handler: {}".format( return "ZB: {} | ETS: {} | Handler: {}".format(
@ -172,8 +171,6 @@ class LegalData(UuidModel):
revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL) revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL)
objects = LegalDataManager()
class Intervention(BaseObject): class Intervention(BaseObject):
""" """

@ -79,7 +79,7 @@ def new_view(request: HttpRequest):
messages.success(request, _("Intervention {} added").format(intervention.identifier)) messages.success(request, _("Intervention {} added").format(intervention.identifier))
return redirect("intervention:detail", id=intervention.id) return redirect("intervention:detail", id=intervention.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass
@ -264,7 +264,7 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("Intervention {} edited").format(intervention.identifier)) messages.success(request, _("Intervention {} edited").format(intervention.identifier))
return redirect("intervention:detail", id=intervention.id) return redirect("intervention:detail", id=intervention.id)
else: else:
messages.error(request, FORM_INVALID) messages.error(request, FORM_INVALID, extra_tags="danger",)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass

@ -37,12 +37,14 @@ class BaseForm(forms.Form):
""" """
template = None template = None
action_url = None action_url = None
action_btn_label = _("Save")
form_title = None form_title = None
cancel_redirect = None cancel_redirect = None
form_caption = None form_caption = None
instance = None # The data holding model object instance = None # The data holding model object
form_attrs = {} # Holds additional attributes, that can be used in the template form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates has_required_fields = False # Automatically set. Triggers hint rendering in templates
show_cancel_btn = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None) self.instance = kwargs.pop("instance", None)
@ -189,6 +191,7 @@ class BaseModalForm(BaseForm, BSModalForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.action_btn_label = _("Continue")
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request """ Generic processing of request

@ -69,6 +69,7 @@ INSTALLED_APPS = [
'user', 'user',
'ema', 'ema',
'codelist', 'codelist',
'analysis',
] ]
if DEBUG: if DEBUG:
INSTALLED_APPS += [ INSTALLED_APPS += [

@ -34,3 +34,19 @@ def bootstrap_cls(value):
""" """
return SVI_BOOTSTRAP_CLS_MAP.get(value, "") return SVI_BOOTSTRAP_CLS_MAP.get(value, "")
@register.filter("default_if_zero")
def default_if_zero(val1, val2):
""" Returns val2 if val1 is 0
Similar to default_if_none
Args:
val1 (int): The numerical value
val2 (str): The alternative
Returns:
"""
return val1 if val1 > 0 else val2

@ -36,7 +36,8 @@ urlpatterns = [
path('ema/', include("ema.urls")), path('ema/', include("ema.urls")),
path('user/', include("user.urls")), path('user/', include("user.urls")),
path('news/', include("news.urls")), path('news/', include("news.urls")),
path('news/', include("codelist.urls")), path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")),
# Generic deadline routes # Generic deadline routes
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"), path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),

@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
FORM_INVALID = _("There was an error on this form.") FORM_INVALID = _("There was an error on this form.")
PARAMS_INVALID = _("Invalid parameters")
INTERVENTION_INVALID = _("There are errors in this intervention.") 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") 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") DATA_UNSHARED = _("This data is not shared with you")

Binary file not shown.

File diff suppressed because it is too large Load Diff

@ -11,11 +11,14 @@ django-filter==2.4.0
django-fontawesome-5==1.0.18 django-fontawesome-5==1.0.18
django-simple-sso==1.1.0 django-simple-sso==1.1.0
django-tables2==2.3.4 django-tables2==2.3.4
et-xmlfile==1.1.0
idna==2.10 idna==2.10
importlib-metadata==2.1.1 importlib-metadata==2.1.1
itsdangerous itsdangerous==0.24
psycopg2-binary openpyxl==3.0.9
psycopg2-binary==2.9.1
pytz==2020.4 pytz==2020.4
qrcode==7.3.1
requests==2.25.0 requests==2.25.0
six==1.15.0 six==1.15.0
soupsieve==2.2.1 soupsieve==2.2.1

@ -19,12 +19,14 @@
{% include 'form/table/generic_table_form_body.html' %} {% include 'form/table/generic_table_form_body.html' %}
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
{% if form.show_cancel_btn %}
<a href="{{ form.cancel_redirect }}"> <a href="{{ form.cancel_redirect }}">
<button class="btn btn-default" type="button" title="{% trans 'Cancel' %}">{% trans 'Cancel' %}</button> <button class="btn btn-default" type="button" title="{% trans 'Cancel' %}">{% trans 'Cancel' %}</button>
</a> </a>
{% endif %}
</div> </div>
<div class="col-6 d-flex justify-content-end"> <div class="col-6 d-flex justify-content-end">
<button class="btn btn-default" type="submit" title="{% trans 'Save' %}">{% trans 'Save' %}</button> <button class="btn btn-default" type="submit" title="{{form.action_btn_label}}">{{form.action_btn_label}}</button>
</div> </div>
</div> </div>
</form> </form>

@ -22,7 +22,7 @@
</div> </div>
{% if form.render_submit %} {% if form.render_submit %}
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-default">{% trans 'Continue' %}</button> <button type="submit" class="btn btn-default" title="{{form.action_btn_label}}">{{form.action_btn_label}}</button>
</div> </div>
{% endif %} {% endif %}
</form> </form>

@ -43,7 +43,7 @@
<a class="dropdown-item" href="{% url 'ema:index' %}" title="{% trans 'Payment funded compensations' %}">{% fa5_icon 'euro-sign' %} {% trans 'EMA' %}</a> <a class="dropdown-item" href="{% url 'ema:index' %}" title="{% trans 'Payment funded compensations' %}">{% fa5_icon 'euro-sign' %} {% trans 'EMA' %}</a>
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-import' %} {% trans 'Import...' %}</a> <a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-import' %} {% trans 'Import...' %}</a>
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-export' %} {% trans 'Export...' %}</a> <a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-export' %} {% trans 'Export...' %}</a>
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-alt' %} {% trans 'Reports' %}</a> <a class="dropdown-item" href="{% url 'analysis:reports' %}">{% fa5_icon 'file-alt' %} {% trans 'Reports' %}</a>
</div> </div>
</li> </li>

Loading…
Cancel
Save