diff --git a/back/app/contract/__init__.py b/back/app/contract/__init__.py index 05c5c670..3556929e 100644 --- a/back/app/contract/__init__.py +++ b/back/app/contract/__init__.py @@ -6,7 +6,7 @@ from ..core.routes import get_crud_router from .routes_draft import draft_router from .print import print_router -from .models import Contract, ContractDraft, Party, replace_variables_in_value +from .models import Contract, ContractDraft, ContractDraftStatus, Party, replace_variables_in_value from .schemas import ContractCreate, ContractRead, ContractUpdate from ..entity.models import Entity @@ -64,6 +64,8 @@ async def create(item: ContractCreate, user=Depends(get_current_user)) -> dict: contract_dict['provisions'] = provisions o = await Contract(**contract_dict).create() + + await draft.update({"$set": {"status": ContractDraftStatus.published}}) return {"message": "Contract Successfully created", "id": o.id} @@ -89,11 +91,11 @@ async def affix_signature(signature_id: str, signature_file: UploadFile = File(. if signature.signature_affixed: raise HTTPException(status_code=400, detail="Signature already affixed") - with open("media/signatures/{}.png".format(signature_id), "wb") as buffer: + with open(f'media/signatures/{signature_id}.png', "wb") as buffer: shutil.copyfileobj(signature_file.file, buffer) update_query = {"$set": { - 'parties.{}.signature_affixed'.format(signature_index): True + f'parties.{signature_index}.signature_affixed': True }} signature.signature_affixed = True if contract.is_signed(): diff --git a/back/app/contract/models.py b/back/app/contract/models.py index 7687278f..7cd35d7e 100644 --- a/back/app/contract/models.py +++ b/back/app/contract/models.py @@ -19,7 +19,7 @@ class ContractStatus(str, Enum): class ContractDraftStatus(str, Enum): in_progress = 'in_progress' ready = 'ready' - created = 'created' + published = 'published' class DraftParty(BaseModel): @@ -29,7 +29,8 @@ class DraftParty(BaseModel): "resource": "entity", "schema": "Entity", } - } + }, + default="" ) part: str representative_id: str = Field( @@ -59,8 +60,8 @@ class Party(BaseModel): class ProvisionGenuine(BaseModel): type: Literal['genuine'] = 'genuine' - title: str = RichtextSingleline(props={"parametrized": True}) - body: str = RichtextMultiline(props={"parametrized": True}) + title: str = RichtextSingleline(props={"parametrized": True}, default="") + body: str = RichtextMultiline(props={"parametrized": True}, default="") class ContractProvisionTemplateReference(BaseModel): @@ -73,7 +74,8 @@ class ContractProvisionTemplateReference(BaseModel): "displayedFields": ['title', 'body'] }, }, - props={"parametrized": True} + props={"parametrized": True}, + default="" ) @@ -98,6 +100,7 @@ class ContractDraft(CrudDocument): format="dictionary", ) status: ContractDraftStatus = ContractDraftStatus.in_progress + todo: List[str] = [] class Settings(CrudDocument.Settings): bson_encoders = { @@ -106,6 +109,40 @@ class ContractDraft(CrudDocument): hour=0, minute=0, second=0) } + async def check_is_ready(self): + 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('Empty variable') + + if self.todo: + self.status = ContractDraftStatus.in_progress + else: + self.status = ContractDraftStatus.ready + + await self.update({"$set": { + "status": self.status, + "todo": self.todo + }}) + class Contract(CrudDocument): name: str diff --git a/back/app/contract/print/__init__.py b/back/app/contract/print/__init__.py index 0ae8a388..d356da39 100644 --- a/back/app/contract/print/__init__.py +++ b/back/app/contract/print/__init__.py @@ -84,8 +84,8 @@ async def preview_draft(draft_id: str) -> str: return await render_print('localhost', draft) -@print_router.get("/preview/{signature_id}", response_class=HTMLResponse) -async def preview_contract(signature_id: str) -> str: +@print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse) +async def preview_contract_by_signature(signature_id: str) -> str: contract = await Contract.find_by_signature_id(signature_id) for p in contract.parties: if p.signature_affixed: @@ -94,6 +94,16 @@ async def preview_contract(signature_id: str) -> str: return await render_print('localhost', contract) +@print_router.get("/preview/{contract_id}", response_class=HTMLResponse) +async def preview_contract(contract_id: str) -> str: + contract = await Contract.get(contract_id) + for p in contract.parties: + if p.signature_affixed: + p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png') + + return await render_print('localhost', contract) + + @print_router.get("/pdf/{contract_id}", response_class=FileResponse) async def create_pdf(contract_id: str) -> str: contract = await Contract.get(contract_id) diff --git a/back/app/contract/routes_draft.py b/back/app/contract/routes_draft.py index 0557156a..97635af1 100644 --- a/back/app/contract/routes_draft.py +++ b/back/app/contract/routes_draft.py @@ -1,7 +1,47 @@ -from fastapi import APIRouter +from beanie import PydanticObjectId +from fastapi import HTTPException, Depends from ..core.routes import get_crud_router -from .models import ContractDraft +from ..user.manager import get_current_user + +from .models import ContractDraft, ContractDraftStatus from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate) + +del(draft_router.routes[0]) +del(draft_router.routes[2]) + + +@draft_router.post("/", response_description="Contract Draft added to the database") +async def create(item: ContractDraftCreate, user=Depends(get_current_user)) -> dict: + await item.validate_foreign_key() + o = await ContractDraft(**item.dict()).create() + await o.check_is_ready() + + return {"message": "Contract Draft added successfully", "id": o.id} + + +@draft_router.put("/{id}", response_description="Contract Draft record updated") +async def update(id: PydanticObjectId, req: ContractDraftUpdate, user=Depends(get_current_user)) -> ContractDraftRead: + req = {k: v for k, v in req.dict().items() if v is not None} + update_query = {"$set": { + field: value for field, value in req.items() + }} + + item = await ContractDraft.get(id) + if not item: + raise HTTPException( + status_code=404, + detail="Contract Draft record not found!" + ) + if item.status == ContractDraftStatus.published: + raise HTTPException( + status_code=400, + detail="Contract Draft has already been published" + ) + + await item.update(update_query) + await item.check_is_ready() + + return ContractDraftRead(**item.dict()) diff --git a/back/app/contract/schemas.py b/back/app/contract/schemas.py index fc7b8943..b62e4928 100644 --- a/back/app/contract/schemas.py +++ b/back/app/contract/schemas.py @@ -3,7 +3,7 @@ from typing import List from pydantic import BaseModel, Field -from .models import ContractDraft, DraftProvision, Party, Contract +from .models import ContractDraft, DraftProvision, DraftParty, Contract from ..entity.models import Entity from ..core.schemas import Writer @@ -17,14 +17,12 @@ class ContractDraftRead(ContractDraft): class ContractDraftCreate(Writer): name: str title: str - parties: List[Party] + parties: List[DraftParty] provisions: List[DraftProvision] variables: List[DictionaryEntry] = Field( default=[], format="dictionary", ) - location: str = "" - date: datetime.date = datetime.date(1, 1, 1) async def validate_foreign_key(self): return diff --git a/front/app/src/app/views/base-view/card/card.component.html b/front/app/src/app/views/base-view/card/card.component.html index 8abf5a9d..b0bb5434 100644 --- a/front/app/src/app/views/base-view/card/card.component.html +++ b/front/app/src/app/views/base-view/card/card.component.html @@ -4,5 +4,6 @@ [schema]="this.schema" (resourceUpdated)="this.flashService.success('Entity updated')" (resourceDeleted)="this.flashService.success('Entity deleted')" + (resourceReceived)="this.onResourceReceived($event)" (error)="this.flashService.error($event)"> \ No newline at end of file diff --git a/front/app/src/app/views/base-view/card/card.component.ts b/front/app/src/app/views/base-view/card/card.component.ts index 61d78a79..453b9579 100644 --- a/front/app/src/app/views/base-view/card/card.component.ts +++ b/front/app/src/app/views/base-view/card/card.component.ts @@ -1,26 +1,32 @@ -import {Component, Input} from "@angular/core"; +import {Component, EventEmitter, Input, Output} from "@angular/core"; import {ActivatedRoute, ParamMap} from "@angular/router"; import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service"; @Component({ - templateUrl: 'card.component.html', - selector: 'base-card', + templateUrl: 'card.component.html', + selector: 'base-card', }) export class BaseCrudCardComponent { - @Input() resource: string | undefined; - @Input() resource_id: string | null = null; - @Input() schema: string | undefined; + @Input() resource: string | undefined; + @Input() resource_id: string | null = null; + @Input() schema: string | undefined; - constructor( - private route: ActivatedRoute, - public flashService: FlashmessagesService - ) {} + @Output() resourceReceived: EventEmitter = new EventEmitter(); - ngOnInit(): void { - if (this.resource_id === null) { - this.route.paramMap.subscribe((params: ParamMap) => { - this.resource_id = params.get('id') - }) + constructor( + private route: ActivatedRoute, + public flashService: FlashmessagesService + ) {} + + ngOnInit(): void { + if (this.resource_id === null) { + this.route.paramMap.subscribe((params: ParamMap) => { + this.resource_id = params.get('id') + }) + } } - } -} \ No newline at end of file + + onResourceReceived(model: any) { + this.resourceReceived.emit(model); + } +} diff --git a/front/app/src/app/views/contracts/contracts.component.ts b/front/app/src/app/views/contracts/contracts.component.ts index bc2b4e0a..329544c4 100644 --- a/front/app/src/app/views/contracts/contracts.component.ts +++ b/front/app/src/app/views/contracts/contracts.component.ts @@ -25,35 +25,54 @@ export class ContractsNewComponent extends BaseContractsComponent { @Component({ template:` - -
- {{ this.contractPrintLink! }}  - -
+ + + + + + + + + [schema]="this.schema" + (resourceReceived)="this.onResourceReceived($event)"> `, }) export class ContractsCardComponent extends BaseContractsComponent{ resource_id: string | null = null; + resourceReadyToPrint = false; contractPrintLink: string | null = null; - constructor( - private route: ActivatedRoute - ) { - super() - } + contractPreviewLink: string | null = null; + + constructor( + private route: ActivatedRoute + ) { + super() + } ngOnInit(): void { if (this.resource_id === null) { this.route.paramMap.subscribe((params: ParamMap) => { this.resource_id = params.get('id') - this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}` + }) } } + + onResourceReceived(model: any): void { + this.resourceReadyToPrint = model.status != "published"; + this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}` + this.contractPreviewLink = `${location.origin}/api/v1/contract/print/preview/${this.resource_id}` + } } @Component({ @@ -119,7 +138,7 @@ export class ContractsSignatureComponent implements OnInit { } getPreview() { - return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/" + this.signature_id); + return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/signature/" + this.signature_id); } postSignature(image: string) { diff --git a/front/app/src/app/views/contracts/drafts.component.ts b/front/app/src/app/views/contracts/drafts.component.ts index 0d7ea58b..47342ccb 100644 --- a/front/app/src/app/views/contracts/drafts.component.ts +++ b/front/app/src/app/views/contracts/drafts.component.ts @@ -3,7 +3,7 @@ import { FormlyFieldConfig } from "@ngx-formly/core"; import { FormGroup} from "@angular/forms"; import { CrudFormlyJsonschemaService } from "@common/crud/crud-formly-jsonschema.service"; import { CrudService } from "@common/crud/crud.service"; -import {ActivatedRoute, ParamMap} from "@angular/router"; +import { ActivatedRoute, ParamMap, Router } from "@angular/router"; import { formatDate } from "@angular/common"; @@ -92,15 +92,20 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit { template: ` + [schema]="this.schema" + (resourceReceived)="this.onResourceReceived($event)" + > Preview - - + + + + ` }) export class DraftsCardComponent extends BaseDraftsComponent implements OnInit { resource_id: string | null = null;templateModel: {} = {}; + isReadyForPublication = false; newContractFormfields: FormlyFieldConfig[] = []; newContractForm: FormGroup = new FormGroup({}); newContractModel: any = { @@ -132,7 +137,8 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit { constructor( private route: ActivatedRoute, private formlyJsonschema: CrudFormlyJsonschemaService, - private crudService: CrudService + private crudService: CrudService, + private router: Router, ) { super(); } @@ -150,8 +156,12 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit { } publish() { - this.crudService.create('contract', this.newContractModel).subscribe((templateModel) => { - console.log(templateModel) + this.crudService.create('contract', this.newContractModel).subscribe((response: any) => { + this.router.navigate([`../../${response.id}`], {relativeTo: this.route}); }); } + + onResourceReceived(model: any): void { + this.isReadyForPublication = model.status == "ready"; + } } diff --git a/front/app/src/common/crud/card/card.component.ts b/front/app/src/common/crud/card/card.component.ts index e00e1530..2c8624e2 100644 --- a/front/app/src/common/crud/card/card.component.ts +++ b/front/app/src/common/crud/card/card.component.ts @@ -37,7 +37,7 @@ export class CardComponent implements OnInit { @Output() resourceUpdated: EventEmitter = new EventEmitter(); @Output() resourceDeleted: EventEmitter = new EventEmitter(); @Output() error: EventEmitter = new EventEmitter(); - + @Output() resourceReceived: EventEmitter = new EventEmitter(); form = new FormGroup({}); fields: FormlyFieldConfig[] = []; @@ -78,6 +78,7 @@ export class CardComponent implements OnInit { next :(model: any) => { this.model = model; this._modelLoading$.next(false); + this.resourceReceived.emit(model); }, error: (err) => this.error.emit("Error loading the model:" + err) }); @@ -96,27 +97,28 @@ export class CardComponent implements OnInit { onSubmit(model: any) { this._modelLoading$.next(true); if (this.isCreateForm()) { - this.crudService.create(this.resource!, model).subscribe({ - next: (response: any) => { - this._modelLoading$.next(false); - if (! this.is_modal) { - this.router.navigate([`../${response.id}`], {relativeTo: this.route}); - } else { - this.resourceCreated.emit(response.id) - } - }, - error: (err) => this.error.emit("Error creating the entity:" + err) - }); + this.crudService.create(this.resource!, model).subscribe({ + next: (response: any) => { + this._modelLoading$.next(false); + if (! this.is_modal) { + this.router.navigate([`../${response.id}`], {relativeTo: this.route}); + } else { + this.resourceCreated.emit(response.id) + } + }, + error: (err) => this.error.emit("Error creating the entity:" + err) + }); } else { model._id = this.resource_id; - this.crudService.update(this.resource!, model).subscribe( { - next: (model: any) => { - this.model = model; - this._modelLoading$.next(false); - this.resourceUpdated.emit(model._id) - }, - error: (err) => this.error.emit("Error updating the entity:" + err) - }); + this.crudService.update(this.resource!, model).subscribe( { + next: (model: any) => { + this.model = model; + this._modelLoading$.next(false); + this.resourceUpdated.emit(model._id); + this.resourceReceived.emit(model); + }, + error: (err) => this.error.emit("Error updating the entity:" + err) + }); } }