Files
roleplay-contract/api/rpk-api/firm/contract/models.py

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: 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 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=None,
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"]