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/eco_account.py

274 lines
8.8 KiB
Python

"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
import shutil
from django.urls import reverse
from konova.utils.message_templates import DEDUCTION_REMOVED
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
from compensation.models.compensation import AbstractCompensation
from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
"""
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.
"""
deductable_surface = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
default=0,
)
legal = models.OneToOneField(
"intervention.Legal",
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on legal dates or law"
)
objects = EcoAccountManager()
def __str__(self):
return f"{self.identifier} ({self.title})"
def clean(self):
# Deductable surface can not be larger than added states after surface
after_state_sum = self.get_state_after_surface_sum()
if self.deductable_surface > after_state_sum:
raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))
# Deductable surface can not be lower than amount of already deducted surfaces
# User needs to contact deducting user in case of further problems
deducted_sum = self.get_deductions_surface()
if self.deductable_surface < deducted_sum:
raise ValidationError(
_("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier if none was given
self.identifier = self.generate_new_identifier()
# Before saving, make sure the given identifier is not used, yet
while EcoAccount.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
@property
def deductions_surface_sum(self) -> float:
""" Shortcut for get_deductions_surface.
Can be used in templates
Returns:
sum_surface (float)
"""
return self.get_deductions_surface()
def get_deductions_surface(self) -> float:
""" Calculates the account's deductions surface sum
Returns:
sum_surface (float)
"""
return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_state_after_surface_sum(self) -> float:
""" Calculates the account's after state surface sum
Returns:
sum_surface (float)
"""
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self) -> (float, float):
""" Calculates available rest surface of the eco account
Args:
Returns:
ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
"""
deductions = self.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val_total = available_surfaces - deductions_surfaces
if available_surfaces > 0:
ret_val_relative = int((ret_val_total / available_surfaces) * 100)
else:
ret_val_relative = 0
return ret_val_total, ret_val_relative
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) -> EcoAccountQualityChecker:
""" Quality check
Returns:
ret_msgs (EcoAccountQualityChecker): Holds validity and error messages
"""
checker = EcoAccountQualityChecker(self)
checker.run_check()
return checker
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
def is_ready_for_publish(self) -> bool:
""" Checks whether the data passes all constraints for being publishable
Returns:
is_ready (bool) : True|False
"""
is_recorded = self.recorded is not None
is_ready = is_recorded
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
return reverse("compensation:acc:share", args=(self.id, self.access_token))
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:
try:
shutil.rmtree(folder_path)
except FileNotFoundError:
# Folder seems to be missing already...
pass
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.Intervention",
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="Deducted for",
related_name="deductions",
)
objects = EcoAccountDeductionManager()
def __str__(self):
return "{} of {}".format(self.surface, self.account)
def delete(self, user=None, *args, **kwargs):
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED, reset_recorded=False)
super().delete(*args, **kwargs)