242 lines
7.8 KiB
Python
242 lines
7.8 KiB
Python
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"]
|