You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
konova/compensation/models.py

445 lines
13 KiB
Python

3 years ago
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20
"""
import shutil
from django.contrib.auth.models import User
3 years ago
from django.contrib.gis.db import models
from django.core.validators import MinValueValidator
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
3 years ago
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
from intervention.models import Intervention, ResponsibilityData
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry
3 years ago
class Payment(BaseResource):
"""
Holds data on a payment for an intervention (alternative to a classic compensation)
"""
amount = models.FloatField(validators=[MinValueValidator(limit_value=0.00)])
due_on = models.DateField(null=True, blank=True)
comment = models.CharField(
max_length=1000,
null=True,
blank=True,
help_text="Refers to german money transfer 'Verwendungszweck'",
)
intervention = models.ForeignKey(
Intervention,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='payments'
)
class Meta:
ordering = [
"-amount",
]
class CompensationState(UuidModel):
3 years ago
"""
Compensations must define the state of an area before and after the compensation.
"""
biotope_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_BIOTOPES_ID],
"is_selectable": True,
"is_archived": False,
}
)
surface = models.FloatField()
3 years ago
def __str__(self):
return "{} | {}".format(self.biotope_type, self.surface)
3 years ago
class UnitChoices(models.TextChoices):
"""
Predefines units for selection
"""
cm = "cm", _("cm")
m = "m", _("m")
km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha")
st = "pcs", _("Pieces") # pieces
3 years ago
class CompensationAction(BaseResource):
"""
Compensations include actions like planting trees, refreshing rivers and so on.
"""
action_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
"is_selectable": True,
"is_archived": False,
}
)
3 years ago
amount = models.FloatField()
unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
comment = models.TextField(blank=True, null=True, help_text="Additional comment")
3 years ago
def __str__(self):
return "{} | {} {}".format(self.action_type, self.amount, self.unit)
@property
def unit_humanize(self):
""" Returns humanized version of enum
Used for template rendering
Returns:
"""
choices = UnitChoices.choices
for choice in choices:
if choice[0] == self.unit:
return choice[1]
return None
3 years ago
class AbstractCompensation(BaseObject):
3 years ago
"""
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount.
3 years ago
"""
responsible = models.OneToOneField(
ResponsibilityData,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler",
)
before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'")
after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'")
actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'")
deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+")
3 years ago
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
3 years ago
class Meta:
abstract = True
def get_surface(self) -> float:
""" Calculates the compensation's/account's surface
Returns:
sum_surface (float)
"""
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"]
class Compensation(AbstractCompensation):
"""
Regular compensation, linked to an intervention
"""
intervention = models.ForeignKey(
Intervention,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='compensations'
)
def __str__(self):
return "{}".format(self.identifier)
3 years ago
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier
new_id = self._generate_new_identifier()
3 years ago
while Compensation.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier()
3 years ago
self.identifier = new_id
super().save(*args, **kwargs)
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation
Returns:
docs (QuerySet): The queryset of all documents
"""
docs = CompensationDocument.objects.filter(
instance=self
)
return docs
3 years ago
class CompensationDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.ForeignKey(
Compensation,
on_delete=models.CASCADE,
related_name="documents",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
def delete(self, *args, **kwargs):
"""
Custom delete functionality for CompensationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
comp_docs = self.instance.get_documents()
folder_path = None
if comp_docs.count() == 1:
# The only file left for this compensation is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
# Remove the file itself
super().delete(*args, **kwargs)
# If a folder path has been set, we need to delete the whole folder!
if folder_path is not None:
shutil.rmtree(folder_path)
class EcoAccount(AbstractCompensation):
3 years ago
"""
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
with some kind of currency. From this account one is able to deduct currency for current projects.
3 years ago
"""
# Users having access on this object
# Not needed in regular Compensation since their access is defined by the linked intervention's access
users = models.ManyToManyField(
User,
help_text="Users having access (shared with)"
)
# Refers to "verzeichnen"
recorded = models.OneToOneField(
UserActionLogEntry,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on user and timestamp of this action",
related_name="+"
)
def __str__(self):
return "{}".format(self.identifier)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier
new_id = self._generate_new_identifier()
while EcoAccount.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier()
self.identifier = new_id
super().save(*args, **kwargs)
def get_deductions_surface(self) -> float:
""" Calculates the account's deductions sum surface
Returns:
sum_surface (float)
"""
return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self, as_percentage: bool = False):
""" Calculates available rest surface of the eco account
Args:
as_percentage (bool): Whether to return the result as or %
Returns:
"""
ret_val = 0
deductions = self.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
after_states_surfaces = self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or deductions_surfaces ## no division by zero
ret_val = after_states_surfaces - deductions_surfaces
if as_percentage:
if after_states_surfaces > 0:
ret_val = int((ret_val / after_states_surfaces) * 100)
else:
ret_val = 0
return ret_val
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def quality_check(self) -> list:
""" Quality check
Returns:
ret_msgs (list): Holds error messages
"""
ret_msgs = []
# ToDo: Add check methods!
return ret_msgs
def get_documents(self) -> QuerySet:
""" Getter for all documents of an EcoAccount
Returns:
docs (QuerySet): The queryset of all documents
"""
docs = EcoAccountDocument.objects.filter(
instance=self
)
return docs
class EcoAccountDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.ForeignKey(
EcoAccount,
on_delete=models.CASCADE,
related_name="documents",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
def delete(self, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
acc_docs = self.instance.get_documents()
folder_path = None
if acc_docs.count() == 1:
# The only file left for this eco account is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
# Remove the file itself
super().delete(*args, **kwargs)
# If a folder path has been set, we need to delete the whole folder!
if folder_path is not None:
shutil.rmtree(folder_path)
class EcoAccountDeduction(BaseResource):
"""
A deduction object for eco accounts
"""
account = models.ForeignKey(
EcoAccount,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Deducted from",
related_name="deductions",
)
surface = models.FloatField(
null=True,
blank=True,
help_text="Amount deducted (m²)",
validators=[
MinValueValidator(limit_value=0.00),
]
)
intervention = models.ForeignKey(
Intervention,
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="Deducted for",
related_name="deductions",
)
def __str__(self):
return "{} of {}".format(self.surface, self.account)