""" Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de Created on: 17.11.20 """ from django.contrib.auth.models import User from django.contrib.gis.db import models from django.core.validators import MinValueValidator from django.db.models import Sum from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from compensation.settings import COMPENSATION_IDENTIFIER_LENGTH, COMPENSATION_IDENTIFIER_TEMPLATE from intervention.models import Intervention, ResponsibilityData from konova.models import BaseObject, BaseResource, Geometry, UuidModel from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.utils.generators import generate_random_string from user.models import UserActionLogEntry 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) 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 CompensationState(UuidModel): """ Compensations must define the state of an area before and after the compensation. """ biotope_type = models.CharField(max_length=500, null=True, blank=True) surface = models.FloatField() def __str__(self): return "{} | {} m²".format(self.biotope_type, self.surface) class UnitChoices(models.TextChoices): """ Predefines units for selection """ cm = "cm", _("cm") m = "m", _("m") km = "km", _("km") qm = "qm", _("m²") ha = "ha", _("ha") st = "pcs", _("Pieces") # pieces class CompensationAction(BaseResource): """ Compensations include actions like planting trees, refreshing rivers and so on. """ action_type = models.CharField(max_length=500, null=True, blank=True) 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") 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 class AbstractCompensation(BaseObject): """ Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation or EcoAccount. """ 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="+") geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) documents = models.ManyToManyField("konova.Document", blank=True) # Holds a successor for this data next_version = models.ForeignKey("Compensation", null=True, blank=True, on_delete=models.DO_NOTHING) 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) @staticmethod def _generate_new_identifier() -> str: """ Generates a new identifier for the intervention object Returns: str """ curr_month = str(now().month) curr_year = str(now().year) rand_str = generate_random_string( length=COMPENSATION_IDENTIFIER_LENGTH, only_numbers=True, ) _str = "{}{}{}".format(curr_month, curr_year, rand_str) return COMPENSATION_IDENTIFIER_TEMPLATE.format(_str) 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 Compensation.objects.filter(identifier=new_id).exists(): new_id = self._generate_new_identifier() 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, ) class EcoAccount(AbstractCompensation): """ 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 'withdraw' currency for current projects. """ # 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 get_surface_withdraws(self) -> float: """ Calculates the compensation's/account's surface Returns: sum_surface (float) """ return self.withdraws.all().aggregate(Sum("surface"))["surface__sum"] 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, ) class EcoAccountWithdraw(BaseResource): """ A withdraw object for eco accounts """ account = models.ForeignKey( EcoAccount, on_delete=models.SET_NULL, null=True, blank=True, help_text="Withdrawn from", related_name="withdraws", ) surface = models.FloatField( null=True, blank=True, help_text="Amount withdrawn (m²)", validators=[ MinValueValidator(limit_value=0.00), ] ) intervention = models.ForeignKey( Intervention, on_delete=models.CASCADE, null=True, blank=True, help_text="Withdrawn for", related_name="withdraws", ) def __str__(self): return "{} of {}".format(self.surface, self.account)