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, ConfigDict from pydantic.json_schema import SkipJsonSchema from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey, \ CrudDocumentConfig 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): model_config = ConfigDict(title='Partie') entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie") part: str = Field(title="Rôle") representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant") entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, ) 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): model_config = ConfigDict(title='Clause personalisée') type: Literal['genuine'] = ContractProvisionType.genuine title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre") body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps") class ContractProvisionTemplateReference(BaseModel): model_config = ConfigDict(title='Template de clause') type: Literal['template'] = ContractProvisionType.template provision_template_id: PydanticObjectId = ForeignKey( "templates/provisions", "ProvisionTemplate", displayed_fields=['title', 'body'], props={"parametrized": True}, default=None, title="Template de clause" ) class DraftProvision(BaseModel): model_config = ConfigDict(title='Clause') provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type') 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 """ model_config = ConfigDict(title='Brouillon de contrat') 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") 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 """ model_config = ConfigDict(title='Contrat') document_config = CrudDocumentConfig( indexes=["parties.signature_uuid"], ) 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 @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"]