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 firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry 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' class ContractDraftStatus(str, Enum): in_progress = 'in_progress' ready = 'ready' published = 'published' class DraftParty(BaseModel): entity_id: PydanticObjectId = Field( foreignKey={ "reference": { "resource": "entity", "schema": "Entity", } }, default="", title="Partie" ) part: str = Field(title="Rôle") representative_id: PydanticObjectId = Field( foreignKey={ "reference": { "resource": "entity", "schema": "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 = Field( foreignKey={ "reference": { "resource": "template/provision", "schema": "ProvisionTemplate", "displayedFields": ['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( props={"items-per-row": "1", "numbered": True}, title='Clauses' ) variables: List[DictionaryEntry] = Field( default=[], format="dictionary", 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) 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"]