import datetime from typing import List, Literal, Optional from enum import Enum from uuid import UUID from beanie import PydanticObjectId from pydantic import BaseModel, Field from pydantic.json_schema import SkipJsonSchema from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey from firm.core.filter import Filter, FilterSchema from firm.entity.models import Entity class ContractStatus(str, Enum): published = 'published' signed = 'signed' printed = 'printed' executed = 'executed' canceled = 'canceled' class ContractDraftStatus(str, Enum): in_progress = 'in_progress' ready = 'ready' published = 'published' class DraftParty(BaseModel): entity_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Partie") entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, ) part: str = Field(title="Rôle") representative_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Représentant") class Config: title = 'Partie' class Party(BaseModel): entity: Entity part: str representative: Optional[Entity] = None signature_uuid: str signature_affixed: bool = False signature_png: Optional[str] = None class ContractProvisionType(Enum): genuine = 'genuine' template = 'template' class ProvisionGenuine(BaseModel): type: Literal['genuine'] = ContractProvisionType.genuine title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre") body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps") class Config: title = 'Clause personalisée' class ContractProvisionTemplateReference(BaseModel): type: Literal['template'] = ContractProvisionType.template provision_template_id: PydanticObjectId = ForeignKey( "templates/provisions", "ProvisionTemplate", displayed_fields=['title', 'body'], props={"parametrized": True}, default="", title="Template de clause" ) class Config: title = 'Template de clause' class DraftProvision(BaseModel): provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type') class Config: title = 'Clause' class Provision(BaseModel): title: str = RichtextSingleline(title="Titre") body: str = RichtextMultiline(title="Corps") class ContractDraftUpdateStatus(BaseModel): status: str = Field() todo: List[str] = Field(default=[]) class ContractDraft(CrudDocument): """ Brouillon de contrat à remplir """ name: str = Field(title="Nom") title: str = Field(title="Titre") parties: List[DraftParty] = Field(title="Parties") provisions: List[DraftProvision] = Field(title='Clauses') variables: List[DictionaryEntry] = Field(default=[], title='Variables') status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut") todo: List[str] = Field(default=[], title="Reste à faire") class Settings(CrudDocument.Settings): fulltext_search = ['name', 'title'] bson_encoders = { datetime.date: lambda dt: dt if hasattr(dt, 'hour') else datetime.datetime(year=dt.year, month=dt.month, day=dt.day, hour=0, minute=0, second=0) } class Config: title = 'Brouillon de contrat' async def check_is_ready(self, db): if self.status == ContractDraftStatus.published: return self.todo = [] if len(self.parties) < 2: self.todo.append('Contract must have at least two parties') if len(self.provisions) < 1: self.todo.append('Contract must have at least one provision') for p in self.parties: if not p.entity_id: self.todo.append('All parties must have an associated entity`') for p in self.provisions: if p.provision.type == "genuine" and not (p.provision.title and p.provision.body): self.todo.append('Empty genuine provision') elif p.provision.type == "template" and not p.provision.provision_template_id: self.todo.append('Empty template provision') for v in self.variables: if not (v.key and v.value): self.todo.append(f'Empty variable: {v.key}') if self.todo: self.status = ContractDraftStatus.in_progress else: self.status = ContractDraftStatus.ready await self.update(db, self, ContractDraftUpdateStatus(status=self.status, todo=self.todo)) async def update_status(self, db, status): update = ContractDraftUpdateStatus(status=status) await self.update(db, self, update) def compute_label(self) -> str: return f"{self.name} - {self.title}" class Contract(CrudDocument): """ Contrat publié. Les contrats ne peuvent pas être modifiés. Ils peuvent seulement être signés par les parties et imprimés par l'avocat """ name: str = Field(title="Nom") title: str = Field(title="Titre") parties: List[Party] = Field(title="Parties") provisions: List[Provision] = Field( props={"items_per_row": "1", "numbered": True}, title='Clauses' ) status: ContractStatus = Field(default=ContractStatus.published, title="Statut") lawyer: Entity = Field(title="Avocat en charge") location: str = Field(title="Lieu") date: datetime.date = Field(title="Date") def compute_label(self) -> str: contract_label = self.title for p in self.parties: contract_label = f"{contract_label} - {p.entity.label}" contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}" return contract_label class Settings(CrudDocument.Settings): fulltext_search = ['name', 'title'] bson_encoders = { datetime.date: lambda dt: dt if hasattr(dt, 'hour') else datetime.datetime(year=dt.year, month=dt.month, day=dt.day, hour=0, minute=0, second=0) } @classmethod async def find_by_signature_id(cls, db, signature_id: UUID): request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}} value = await cls._get_collection(db).find_one(request) return cls.model_validate(value) if value else None def get_signature(self, signature_id: str): for p in self.parties: if p.signature_uuid == str(signature_id): return p def get_signature_index(self, signature_id: str): for i, p in enumerate(self.parties): if p.signature_uuid == str(signature_id): return i def is_signed(self): for p in self.parties: if not p.signature_affixed: return False return True async def affix_signature(self, db, signature_index): update_query = {"$set": { f'parties.{signature_index}.signature_affixed': True }} self.parties[signature_index].signature_affixed = True if self.is_signed(): update_query["$set"]['status'] = 'signed' await self._get_collection(db).update_one({"_id": self.id}, update_query) return await self.get(db, self.id) def replace_variables_in_value(variables, value: str): for v in variables: if v.value: value = value.replace('%{}%'.format(v.key), v.value) return value class ContractDraftFilters(FilterSchema): status: Optional[str] = None class Constants(Filter.Constants): model = ContractDraft search_model_fields = ["label", "status"] class ContractFilters(FilterSchema): status: Optional[str] = None class Constants(Filter.Constants): model = Contract search_model_fields = ["label", "status"]