mpeltriaux
7b3b40f3c9
* adds direct object links into mail templates * refactors transferring app-model identification data from fore- to background (celery) properly
313 lines
9.7 KiB
Python
313 lines
9.7 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 compensation.settings import ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH, \
|
|
ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED, ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED
|
|
from konova.sub_settings.django_settings import BASE_URL
|
|
from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE
|
|
from django.core.validators import MinValueValidator
|
|
from django.db import models
|
|
from django.db.models import Sum, QuerySet
|
|
|
|
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
|
|
from compensation.models.compensation import AbstractCompensation, PikMixin
|
|
from compensation.utils.quality import EcoAccountQualityChecker
|
|
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
|
|
generate_document_file_upload_path
|
|
from konova.tasks import celery_send_mail_deduction_changed, celery_send_mail_deduction_changed_team
|
|
|
|
|
|
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
|
|
"""
|
|
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,
|
|
)
|
|
deductable_rest = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text="Amount of deductable rest",
|
|
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()
|
|
|
|
identifier_length = ECO_ACCOUNT_IDENTIFIER_LENGTH
|
|
identifier_template = ECO_ACCOUNT_IDENTIFIER_TEMPLATE
|
|
|
|
def __str__(self):
|
|
return f"{self.identifier} ({self.title})"
|
|
|
|
def get_detail_url(self):
|
|
return reverse("compensation:acc:detail", args=(self.id,))
|
|
|
|
def get_detail_url_absolute(self):
|
|
return BASE_URL + self.get_detail_url()
|
|
|
|
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)
|
|
"""
|
|
val = self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
|
|
val = float('{:0.2f}'.format(val))
|
|
return val
|
|
|
|
def __calculate_deductable_rest(self):
|
|
""" Calculates available rest surface of the eco account
|
|
|
|
Args:
|
|
|
|
Returns:
|
|
ret_val_total (float): Total amount
|
|
"""
|
|
deductions_surfaces = self.get_deductions_surface()
|
|
|
|
available_surface = self.deductable_surface
|
|
if available_surface is None:
|
|
# Fallback!
|
|
available_surface = deductions_surfaces
|
|
else:
|
|
available_surface = float(available_surface)
|
|
|
|
ret_val = available_surface - deductions_surfaces
|
|
|
|
return ret_val
|
|
|
|
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-token", args=(self.id, self.access_token))
|
|
|
|
def send_notification_mail_on_deduction_change(self, data_change: dict):
|
|
""" Sends notification mails for changes on the deduction
|
|
|
|
Args:
|
|
data_change ():
|
|
|
|
Returns:
|
|
|
|
"""
|
|
# Send mail
|
|
shared_users = self.shared_users.values_list("id", flat=True)
|
|
for user_id in shared_users:
|
|
celery_send_mail_deduction_changed.delay(self.id, self.get_app_object_tuple(), user_id, data_change)
|
|
|
|
# Send mail
|
|
shared_teams = self.shared_teams.values_list("id", flat=True)
|
|
for team_id in shared_teams:
|
|
celery_send_mail_deduction_changed_team.delay(self.id, self.get_app_object_tuple(), team_id, data_change)
|
|
|
|
def update_deductable_rest(self):
|
|
"""
|
|
Updates deductable_rest, which holds the amount of rest surface for this account.
|
|
|
|
Returns:
|
|
|
|
"""
|
|
self.deductable_rest = self.__calculate_deductable_rest()
|
|
self.save()
|
|
|
|
def get_deductable_rest_relative(self):
|
|
"""
|
|
Returns deductable_rest relative to deductable_surface mapped to [0,100]
|
|
|
|
Returns:
|
|
|
|
"""
|
|
try:
|
|
ret_val = int((self.deductable_rest / (self.deductable_surface or 0)) * 100)
|
|
except ZeroDivisionError:
|
|
ret_val = 0
|
|
return ret_val
|
|
|
|
def get_lanis_layer_name(self):
|
|
""" Getter for specific LANIS/WFS object layer
|
|
|
|
Returns:
|
|
|
|
"""
|
|
retval = None
|
|
if self.is_recorded:
|
|
retval = ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED
|
|
else:
|
|
retval = ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED
|
|
|
|
return retval
|
|
|
|
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, user=None, *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
|
|
try:
|
|
folder_path = self.file.path.split("/")[:-1]
|
|
folder_path = "/".join(folder_path)
|
|
except ValueError:
|
|
folder_path = None
|
|
|
|
if user:
|
|
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
|
|
|
|
# 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)
|
|
super().delete(*args, **kwargs)
|
|
self.account.update_deductable_rest()
|
|
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
self.account.update_deductable_rest()
|