#26 Annual conservation report
* introduces new app 'analysis' for annual report generating and future features * adds new templates (WIP) * adds new routes (WIP)
This commit is contained in:
0
analysis/__init__.py
Normal file
0
analysis/__init__.py
Normal file
3
analysis/admin.py
Normal file
3
analysis/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
analysis/apps.py
Normal file
5
analysis/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AnalysisConfig(AppConfig):
|
||||
name = 'analysis'
|
||||
3
analysis/models.py
Normal file
3
analysis/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
17
analysis/templates/analysis/reports/detail.html
Normal file
17
analysis/templates/analysis/reports/detail.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n fontawesome_5 %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
<h3>{% trans 'Report' %}</h3>
|
||||
<h5>{{office.long_name}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
{% include 'analysis/reports/includes/intervention/card_intervention.html' %}
|
||||
{% include 'analysis/reports/includes/card_compensation.html' %}
|
||||
{% include 'analysis/reports/includes/card_eco_account.html' %}
|
||||
{% include 'analysis/reports/includes/card_old_interventions.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 'form/table/generic_table_form_body.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="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 'form/table/generic_table_form_body.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 'form/table/generic_table_form_body.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load i18n fontawesome_5 %}
|
||||
|
||||
<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">{% trans 'Total' %}</th>
|
||||
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{report.intervention_report.queryset.count}}</td>
|
||||
<td>{{report.intervention_report.queryset_checked.count}}</td>
|
||||
<td>{{report.intervention_report.queryset_recorded.count}}</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/laws.html' %}
|
||||
<hr>
|
||||
{% include 'analysis/reports/includes/intervention/compensated_by.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
{% load i18n fontawesome_5 %}
|
||||
<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">{% trans 'Total' %}</th>
|
||||
<th class="w-25" scope="col">{% trans 'Checked' %}</th>
|
||||
<th class="w-25" scope="col">{% trans 'Recorded' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans 'Compensation' %}</th>
|
||||
<td>{{report.intervention_report.compensation_sum}}</td>
|
||||
<td>{{report.intervention_report.compensation_sum_checked}}</td>
|
||||
<td>{{report.intervention_report.compensation_sum_recorded}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans 'Payment' %}</th>
|
||||
<td>{{report.intervention_report.payment_sum}}</td>
|
||||
<td>{{report.intervention_report.payment_sum_checked}}</td>
|
||||
<td>{{report.intervention_report.payment_sum_recorded}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans 'Deductions' %}</th>
|
||||
<td>{{report.intervention_report.deduction_sum}}</td>
|
||||
<td>{{report.intervention_report.deduction_sum_checked}}</td>
|
||||
<td>{{report.intervention_report.deduction_sum_recorded}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
{% load i18n fontawesome_5 %}
|
||||
<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">
|
||||
{% trans 'Checked' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% 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}}</td>
|
||||
<td>{{law.num_recorded}}</td>
|
||||
<td>{{law.num}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td><strong>{% trans 'Total' %}</strong></td>
|
||||
<td><strong>{{report.intervention_report.law_sum_checked}}</strong></td>
|
||||
<td><strong>{{report.intervention_report.law_sum_recorded}}</strong></td>
|
||||
<td><strong>{{report.intervention_report.law_sum}}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
8
analysis/templates/analysis/reports/index.html
Normal file
8
analysis/templates/analysis/reports/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n fontawesome_5 %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<h3>{% trans 'Reports' %}</h3>
|
||||
</div>
|
||||
{% endblock %}
|
||||
3
analysis/tests.py
Normal file
3
analysis/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
15
analysis/urls.py
Normal file
15
analysis/urls.py
Normal file
@@ -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"),
|
||||
]
|
||||
127
analysis/utils/report.py
Normal file
127
analysis/utils/report.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 18.10.21
|
||||
|
||||
"""
|
||||
from django.db.models import Count, Sum, Q
|
||||
|
||||
from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_LAW_ID
|
||||
from compensation.models import Compensation, Payment, EcoAccountDeduction
|
||||
from intervention.models import Intervention
|
||||
|
||||
|
||||
class TimespanReport:
|
||||
office_id = -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
|
||||
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
|
||||
|
||||
def __init__(self, id: str):
|
||||
self.queryset = Intervention.objects.filter(
|
||||
responsible__conservation_office__id=id,
|
||||
deleted=None,
|
||||
)
|
||||
self.queryset_checked = self.queryset.filter(
|
||||
checked__isnull=False
|
||||
)
|
||||
self.queryset_recorded = self.queryset.filter(
|
||||
recorded__isnull=False
|
||||
)
|
||||
self._create_report()
|
||||
|
||||
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()
|
||||
|
||||
def __init__(self, office_id: str):
|
||||
self.office_id = office_id
|
||||
self.intervention_report = self.InterventionReport(self.office_id)
|
||||
59
analysis/views.py
Normal file
59
analysis/views.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from analysis.utils.report import TimespanReport
|
||||
from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_LAW_ID
|
||||
from compensation.models import EcoAccount, Compensation
|
||||
from ema.models import Ema
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import conservation_office_group_required
|
||||
|
||||
|
||||
@login_required
|
||||
@conservation_office_group_required
|
||||
def index_reports_view(request: HttpRequest):
|
||||
"""
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
template = "analysis/reports/index.html"
|
||||
context = {}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
def detail_report_view(request: HttpRequest, id: str):
|
||||
cons_office = get_object_or_404(
|
||||
KonovaCode,
|
||||
id=id,
|
||||
)
|
||||
cons_interventions = Intervention.objects.filter(
|
||||
responsible__conservation_office__id=id,
|
||||
deleted=None,
|
||||
)
|
||||
cons_comps = Compensation.objects.filter(
|
||||
intervention__in=cons_interventions,
|
||||
deleted=None,
|
||||
)
|
||||
cons_eco_account = EcoAccount.objects.filter(
|
||||
responsible__conservation_office__id=id,
|
||||
deleted=None,
|
||||
)
|
||||
|
||||
report = TimespanReport(id)
|
||||
|
||||
template = "analysis/reports/detail.html"
|
||||
context = {
|
||||
"office": cons_office,
|
||||
"report": report,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
Reference in New Issue
Block a user