import datetime import typing from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import reverse from markdownx.models import MarkdownxField from shared.models import NameStrMixin from users.models import User class OwnPermissionsMixin(models.Model): class Meta: abstract = True permissions = [ ("edit_others", "Upravit cizí"), ("delete_others", "Odstranit cizí"), ] class ContractCountMixin(models.Model): def get_contract_count(self, user) -> None: filter = {"is_approved": True} if not user.has_perm("contract.view_confidential"): filter["is_public"] = True return self.contracts.filter(**filter).count() class Meta: abstract = True class SignatureCountMixin(models.Model): def get_signature_count(self, user) -> None: filter = {"contract__is_approved": True} if not user.has_perm("contract.view_confidential"): filter["contract__is_approved"] = True return self.signatures.filter(**filter).count() class Meta: abstract = True class CreatedByMixin(models.Model): created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, related_name="+", verbose_name="Vytvořeno uživatelem", ) # WARNING: exclude in admin class Meta: abstract = True class RepresentativeMixin: @property def name(self): raise NotImplementedError @property def function(self): raise NotImplementedError def __str__(self) -> str: result = self.name if self.function is not None: result += f", {self.function}" return result class Signee(CreatedByMixin, OwnPermissionsMixin, SignatureCountMixin, models.Model): name = models.CharField( max_length=256, verbose_name="Jméno", ) class EntityTypes(models.TextChoices): NATURAL_PERSON = "natural_person", "Fyzická osoba" LEGAL_ENTITY = "legal_entity", "Právnická osoba" BUSINESS_NATURAL_PERSON = "business_natural_person", "Podnikající fyzická osoba" OTHER = "other", "Jiné" entity_type = models.CharField( max_length=23, choices=EntityTypes.choices, default=EntityTypes.LEGAL_ENTITY, verbose_name="Typ", help_text="Důležité označit správně! Fyzickým osobám nepublikujeme adresu.", ) address_street_with_number = models.CharField( max_length=256, verbose_name="Ulice, č.p.", help_text="Veřejné pouze, když typ není nastaven na fyzickou osobu.", ) # WARNING: Legal entity status dependent! address_district = models.CharField( max_length=256, verbose_name="Obec", ) address_zip = models.CharField( max_length=16, verbose_name="PSČ", help_text="Veřejné pouze, když typ není nastaven na fyzickou osobu.", ) # WARNING: Legal entity status dependent! address_country = models.CharField( max_length=256, verbose_name="Země", default=settings.DEFAULT_COUNTRY, ) ico_number = models.CharField( max_length=16, blank=True, null=True, verbose_name="IČO", help_text="Vyplněním můžeš automaticky načíst data z ARES.", ) # WARNING: Legal entity status dependent! date_of_birth = models.DateField( blank=True, null=True, verbose_name="Datum narození", ) # WARNING: Legal entity status dependent! department = models.CharField( max_length=128, blank=True, null=True, verbose_name="Organizační složka", ) role = models.CharField( max_length=256, blank=True, null=True, verbose_name="Role", ) @property def url(self) -> str: return reverse("contracts:view_signee", args=(self.id,)) @property def entity_has_public_address(self) -> bool: return self.entity_type in ( self.EntityTypes.LEGAL_ENTITY, self.EntityTypes.OTHER, ) def __str__(self) -> str: result = self.name if self.ico_number is not None: result += f" ({self.ico_number})" if self.date_of_birth is not None: result += f" ({self.date_of_birth})" if self.department is not None: result += f", {self.department}" return result class Meta: app_label = "contracts" verbose_name = "Jiná smluvní strana" verbose_name_plural = "Ostatní smluvní strany" permissions = OwnPermissionsMixin.Meta.permissions class Contractee( CreatedByMixin, OwnPermissionsMixin, SignatureCountMixin, models.Model ): name = models.CharField( max_length=256, default=settings.DEFAULT_CONTRACTEE_NAME, verbose_name="Jméno", ) address_street_with_number = models.CharField( max_length=256, default=settings.DEFAULT_CONTRACTEE_STREET, verbose_name="Ulice, č.p.", ) address_district = models.CharField( max_length=256, default=settings.DEFAULT_CONTRACTEE_DISTRICT, verbose_name="Obec", ) address_zip = models.CharField( max_length=16, default=settings.DEFAULT_CONTRACTEE_ZIP, verbose_name="PSČ", ) address_country = models.CharField( max_length=256, verbose_name="Země", default=settings.DEFAULT_COUNTRY, ) ico_number = models.CharField( max_length=16, blank=True, null=True, default=settings.DEFAULT_CONTRACTEE_ICO_NUMBER, verbose_name="IČO", ) department = models.CharField( max_length=128, blank=True, null=True, verbose_name="Organizační složka", ) role = models.CharField( max_length=256, blank=True, null=True, verbose_name="Role", ) @property def url(self) -> str: return reverse("contracts:view_contractee", args=(self.id,)) def __str__(self) -> str: result = self.name if self.department is not None: result += f", {self.department}" return result class Meta: app_label = "contracts" verbose_name = "Naše smluvní strana" verbose_name_plural = "Naše smluvní strany" permissions = OwnPermissionsMixin.Meta.permissions class ContractType(ContractCountMixin, NameStrMixin, models.Model): name = models.CharField( max_length=32, verbose_name="Jméno", ) @property def url(self) -> str: return reverse("contracts:view_contract_type", args=(self.id,)) class Meta: app_label = "contracts" verbose_name = "Typ smlouvy" verbose_name_plural = "Typy smluv" class ContractIssue(ContractCountMixin, NameStrMixin, models.Model): name = models.CharField( max_length=32, verbose_name="Jméno", ) @property def url(self) -> str: return reverse("contracts:view_contract_issue", args=(self.id,)) class Meta: app_label = "contracts" verbose_name = "Problém se smlouvou" verbose_name_plural = "Problémy se smlouvami" class ContractFilingArea(ContractCountMixin, NameStrMixin, models.Model): name = models.CharField( max_length=32, verbose_name="Jméno", ) person_responsible = models.CharField( max_length=256, verbose_name="Odpovědná osoba", ) @property def url(self) -> str: return reverse("contracts:view_contract_filing_area", args=(self.id,)) class Meta: app_label = "contracts" verbose_name = "Spisovna" verbose_name_plural = "Spisovny" def get_created_on_timestamp(): return datetime.datetime.now(datetime.timezone.utc) class Contract(NameStrMixin, models.Model): # BEGIN Automatically set fields created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, related_name="uploaded_contracts", verbose_name="Vytvořena uživatelem", help_text="Informace není veřejně přístupná. Pokud vytváříš novou smlouvu, budeš to ty.", ) # WARNING: exclude in admin created_on = models.DateTimeField( blank=False, null=False, default=get_created_on_timestamp, verbose_name="Čas vytvoření", ) public_status_set_by = models.ForeignKey( User, on_delete=models.SET_NULL, blank=True, null=True, related_name="public_status_altered_contracts", verbose_name="Zveřejněno / nezveřejněno uživatelem", help_text="Obsah není veřejně přístupný.", ) # WARNING: exclude in admin all_parties_sign_date = models.DateField( verbose_name="Datum podpisu všech stran", blank=True, null=True, ) # WARNING: Exclude in admin, autofill # END Automatically set fields # BEGIN Approval fields is_approved = models.BooleanField( verbose_name="Je schválená", help_text=( "Mohou měnit jen schvalovatelé. Pokud je " "smlouva veřejná, schválením se vypustí ven." ), ) # END Approval fields name = models.CharField( max_length=128, verbose_name="Název", ) id_number = models.CharField( max_length=128, blank=True, null=True, verbose_name="Identifikační číslo", ) types = models.ManyToManyField( ContractType, related_name="contracts", verbose_name="Typ", ) summary = models.TextField( max_length=256, blank=True, null=True, verbose_name="Sumarizace obsahu smlouvy", ) valid_start_date = models.DateField( blank=True, null=True, verbose_name="Začátek účinnosti", ) valid_end_date = models.DateField( blank=True, null=True, verbose_name="Konec účinnosti", ) class LegalStates(models.TextChoices): VALID = "valid", "Platná" INVALID = "invalid", "Neplatná" class PublicStates(models.TextChoices): YES = "yes", "Veřejná" NO = "no", "Neveřejná" class PaperFormStates(models.TextChoices): SENT = "sent", "Odeslaný" STORED = "stored", "Uložený" TO_SHRED = "to_shred", "Ke skartaci" SHREDDED = "shredded", "Skartovaný" LOST = "lost", "Ztracený" legal_state = models.CharField( max_length=13, choices=LegalStates.choices, verbose_name="Stav právního ujednání", ) is_public = models.BooleanField( verbose_name="Je veřejná", help_text=( "Neveřejné smlouvy nejsou vidět bez přihlášení " "jako min. tajný čtenář." ), ) paper_form_state = models.CharField( max_length=8, choices=PaperFormStates.choices, verbose_name="Stav fyzického dokumentu", ) publishing_rejection_comment = models.CharField( max_length=65536, blank=True, null=True, verbose_name="Důvod nezveřejnění", help_text="Obsah není veřejně přístupný.", ) # WARNING: public status dependent tender_url = models.URLField( max_length=256, blank=True, null=True, verbose_name="Odkaz na výběrové řízení", ) agreement_url = models.URLField( max_length=256, blank=True, null=True, verbose_name="Odkaz na schválení", ) # WARNING: Dependent on the type! issues = models.ManyToManyField( ContractIssue, blank=True, related_name="contracts", verbose_name="Problémy", help_text='Veřejně nazváno "Poznámky".', ) class CostUnits(models.TextChoices): HOUR = "hour", "Hodina" MONTH = "month", "Měsíc" YEAR = "year", "Rok" TOTAL = "total", "Celkem" cost_amount = models.PositiveIntegerField( blank=True, null=True, verbose_name="Náklady (Kč)" ) cost_unit = models.CharField( max_length=5, choices=CostUnits.choices, blank=True, null=True, verbose_name="Jednotka nákladů", ) filing_area = models.ForeignKey( ContractFilingArea, on_delete=models.SET_NULL, blank=True, null=True, related_name="contracts", verbose_name="Spisovna", help_text="Obsah není veřejně přístupný.", ) primary_contract = models.ForeignKey( "Contract", on_delete=models.SET_NULL, # TODO: Figure out if we want this behavior blank=True, null=True, related_name="subcontracts", verbose_name="Primární smlouva", help_text="Např. pro dodatky nebo objednávky u rámcových smluv.", ) # WARNING: Dependent on the type! notes = MarkdownxField( blank=True, null=True, verbose_name="Poznámky", help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat a pro tajné čtenáře.", ) @property def primary_contract_url(self) -> typing.Union[None, str]: if self.primary_contract is None: return return reverse( "contracts:view_contract", args=(self.primary_contract.id,), ) @property def url(self) -> str: return reverse( "contracts:view_contract", args=(self.id,), ) def get_public_files(self): return ContractFile.objects.filter( contract=self, is_public=True, ).all() def get_private_files(self): return ContractFile.objects.filter( contract=self, is_public=False, ).all() def calculate_signing_parties_sign_date(self) -> None: contractees_latest_sign_date = None signees_latest_sign_date = None for contractee_signature in self.contractee_signatures.all(): if ( contractees_latest_sign_date is None or contractee_signature.date > contractees_latest_sign_date ): contractees_latest_sign_date = contractee_signature.date for signee_signature in self.signee_signatures.all(): if ( signees_latest_sign_date is None or signee_signature.date > signees_latest_sign_date ): signees_latest_sign_date = signee_signature.date if ( contractees_latest_sign_date is not None and signees_latest_sign_date is not None ): self.all_parties_sign_date = max( (contractees_latest_sign_date, signees_latest_sign_date) ) self.save() def clean(self): if ( self.primary_contract is not None and self.is_public and not self.primary_contract.is_public ): raise ValidationError( { "is_public": "Primární smlouva je neveřejná, tato smlouva nemůže být veřejná." } ) class Meta: app_label = "contracts" verbose_name = "Smlouva" verbose_name_plural = "Smlouvy" permissions = [ ("approve", "Schválit / zrušit schválení"), ("view_confidential", "Zobrazit tajné informace"), ("edit_when_approved", "Upravit schválené"), ("delete_when_approved", "Odstranit schválené"), ] + OwnPermissionsMixin.Meta.permissions class ContractFile(NameStrMixin, models.Model): name = models.CharField( max_length=128, blank=True, null=True, verbose_name="Jméno", ) is_public = models.BooleanField( verbose_name="Veřejně dostupný", default=False, ) file = models.FileField( verbose_name="Soubor", upload_to="_private/", ) contract = models.ForeignKey( Contract, on_delete=models.CASCADE, related_name="files", verbose_name="Soubory", ) @property def protected_url(self) -> str: return reverse( "contracts:download_contract_file", args=(self.id,), ) class Meta: app_label = "contracts" verbose_name = "Soubor" verbose_name_plural = "Soubory" class ContracteeSignature(models.Model): contractee = models.ForeignKey( Contractee, on_delete=models.CASCADE, related_name="signatures", verbose_name="Smluvní strana", ) contract = models.ForeignKey( Contract, on_delete=models.CASCADE, related_name="contractee_signatures", verbose_name="Podpisy našich smluvních stran", ) date = models.DateField( verbose_name="Datum podpisu", ) def __str__(self) -> str: return f"{str(self.contractee)} - {self.date}" class Meta: app_label = "contracts" verbose_name = "Podpis naší smluvní strany" verbose_name_plural = "Podpisy našich smluvních stran" class SigneeSignature(models.Model): signee = models.ForeignKey( Signee, on_delete=models.CASCADE, related_name="signatures", verbose_name="Smluvní strana", ) contract = models.ForeignKey( Contract, on_delete=models.CASCADE, related_name="signee_signatures", verbose_name="Podpisy jiných smluvních stran", ) date = models.DateField( verbose_name="Datum podpisu", ) def __str__(self) -> str: return f"{str(self.signee)} - {self.date}" class Meta: app_label = "contracts" verbose_name = "Podpis jiné smluvní strany" verbose_name_plural = "Podpisy jiných smluvních stran" class ContracteeSignatureRepresentative(RepresentativeMixin, models.Model): contractee_signature = models.ForeignKey( ContracteeSignature, on_delete=models.CASCADE, related_name="representatives", verbose_name="Podpis naši smluvní strany", ) name = models.CharField( max_length=256, verbose_name="Jméno", ) function = models.CharField( max_length=256, blank=True, null=True, verbose_name="Funkce", ) class Meta: app_label = "contracts" verbose_name = "Zástupce naší smluvní strany" verbose_name_plural = "Zástupci naší smluvní strany" class SigneeSignatureRepresentative(RepresentativeMixin, models.Model): signee_signature = models.ForeignKey( SigneeSignature, on_delete=models.CASCADE, related_name="representatives", verbose_name="Podpis jiné smluvní strany", ) name = models.CharField( max_length=256, verbose_name="Jméno", ) function = models.CharField( max_length=256, blank=True, null=True, verbose_name="Funkce", ) class Meta: app_label = "contracts" verbose_name = "Zástupce jiné smluvní strany" verbose_name_plural = "Zástupci jiné smluvní strany" @receiver(post_save, sender=ContracteeSignature) @receiver(post_save, sender=SigneeSignature) def signing_parties_post_save_update_dates(sender, instance, *args, **kwargs) -> None: instance.contract.calculate_signing_parties_sign_date() class ContractIntent(NameStrMixin, models.Model): name = models.CharField( max_length=128, verbose_name="Jméno", ) url = models.URLField( max_length=256, verbose_name="Odkaz", ) contract = models.ForeignKey( Contract, on_delete=models.CASCADE, related_name="intents", verbose_name="Smlouva", ) class Meta: app_label = "contracts" verbose_name = "Záměr" verbose_name_plural = "Záměry"