diff --git a/back/app/contract/__init__.py b/back/app/contract/__init__.py index 290647b5..05c5c670 100644 --- a/back/app/contract/__init__.py +++ b/back/app/contract/__init__.py @@ -1,11 +1,12 @@ import uuid -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, File, UploadFile +import shutil from ..core.routes import get_crud_router from .routes_draft import draft_router from .print import print_router -from .models import Contract, ContractDraft, replace_variables_in_value +from .models import Contract, ContractDraft, Party, replace_variables_in_value from .schemas import ContractCreate, ContractRead, ContractUpdate from ..entity.models import Entity @@ -72,10 +73,31 @@ async def update(id: str, contract_form: ContractUpdate, user=Depends(get_curren @contract_router.get("/signature/{signature_id}", response_description="") -async def get_signature(signature_id: str) -> ContractRead: - raise HTTPException(status_code=500, detail="Not implemented") +async def get_signature(signature_id: str) -> Party: + contract = await Contract.find_by_signature_id(signature_id) + signature = contract.get_signature(signature_id) + return signature @contract_router.post("/signature/{signature_id}", response_description="") -async def affix_signature(signature_id: str, signature_form: ContractCreate) -> ContractRead: - raise HTTPException(status_code=500, detail="Not implemented") +async def affix_signature(signature_id: str, signature_file: UploadFile = File(...)) -> bool: + contract = await Contract.find_by_signature_id(signature_id) + + signature_index = contract.get_signature_index(signature_id) + signature = contract.parties[signature_index] + + if signature.signature_affixed: + raise HTTPException(status_code=400, detail="Signature already affixed") + + with open("media/signatures/{}.png".format(signature_id), "wb") as buffer: + shutil.copyfileobj(signature_file.file, buffer) + + update_query = {"$set": { + 'parties.{}.signature_affixed'.format(signature_index): True + }} + signature.signature_affixed = True + if contract.is_signed(): + update_query["$set"]['status'] = 'signed' + await contract.update(update_query) + + return True diff --git a/back/app/contract/models.py b/back/app/contract/models.py index 4ee2bc9a..7687278f 100644 --- a/back/app/contract/models.py +++ b/back/app/contract/models.py @@ -1,10 +1,9 @@ -import uuid - import datetime from typing import List, Literal from enum import Enum from pydantic import BaseModel, Field +from beanie.operators import ElemMatch from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry from ..entity.models import Entity @@ -13,6 +12,7 @@ from ..entity.models import Entity class ContractStatus(str, Enum): published = 'published' signed = 'signed' + printed = 'printed' executed = 'executed' @@ -54,6 +54,7 @@ class Party(BaseModel): representative: Entity = None signature_uuid: str signature_affixed: bool = False + signature_png: str = None class ProvisionGenuine(BaseModel): @@ -127,6 +128,27 @@ class Contract(CrudDocument): hour=0, minute=0, second=0) } + @classmethod + def find_by_signature_id(cls, signature_id: str): + crit = ElemMatch(cls.parties, {"signature_uuid": signature_id}) + return cls.find_one(crit) + + def get_signature(self, signature_id: str): + for p in self.parties: + if p.signature_uuid == signature_id: + return p + + def get_signature_index(self, signature_id: str): + for i, p in enumerate(self.parties): + if p.signature_uuid == signature_id: + return i + + def is_signed(self): + for p in self.parties: + if not p.signature_affixed: + return False + return True + def replace_variables_in_value(variables, value: str): for v in variables: diff --git a/back/app/contract/print/__init__.py b/back/app/contract/print/__init__.py index f79f2893..0ae8a388 100644 --- a/back/app/contract/print/__init__.py +++ b/back/app/contract/print/__init__.py @@ -1,11 +1,11 @@ import datetime +import os +import base64 -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException from fastapi.responses import HTMLResponse, FileResponse from fastapi.templating import Jinja2Templates -from beanie.operators import ElemMatch - from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration @@ -13,7 +13,7 @@ from pathlib import Path from app.entity.models import Entity from app.template.models import ProvisionTemplate -from ..models import ContractDraft, Contract, replace_variables_in_value +from ..models import ContractDraft, Contract, ContractStatus, replace_variables_in_value async def build_model(model): @@ -77,32 +77,55 @@ async def render_css(host, contract): }) -@print_router.get("/preview/draft/{draftId}", response_class=HTMLResponse) -async def preview_draft(draftId: str) -> str: - draft = await build_model(await ContractDraft.get(draftId)) +@print_router.get("/preview/draft/{draft_id}", response_class=HTMLResponse) +async def preview_draft(draft_id: str) -> str: + draft = await build_model(await ContractDraft.get(draft_id)) return await render_print('localhost', draft) @print_router.get("/preview/{signature_id}", response_class=HTMLResponse) async def preview_contract(signature_id: str) -> str: - crit = ElemMatch(Contract.parties, {"signature_uuid": signature_id}) - contract = await Contract.find_one(crit) + contract = await Contract.find_by_signature_id(signature_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", response_class=FileResponse) -async def create_pdf() -> str: - draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4")) +@print_router.get("/pdf/{contract_id}", response_class=FileResponse) +async def create_pdf(contract_id: str) -> str: + contract = await Contract.get(contract_id) + contract_path = "media/contracts/{}.pdf".format(contract_id) + if not os.path.isfile(contract_path): + if contract.status != ContractStatus.signed: + raise HTTPException(status_code=400, detail="Contract is not in a printable state") - font_config = FontConfiguration() - html = HTML(string=await render_print('nginx', draft)) - css = CSS(string=await render_css('nginx', draft), font_config=font_config) + for p in contract.parties: + signature_path = f'media/signatures/{p.signature_uuid}.png' + p.signature_png = retrieve_signature_png(signature_path) + # os.remove(signature_path) - html.write_pdf('out.pdf', stylesheets=[css], font_config=font_config) + font_config = FontConfiguration() + html = HTML(string=await render_print('nginx', contract)) + css = CSS(string=await render_css('nginx', contract), font_config=font_config) + + html.write_pdf(contract_path, stylesheets=[css], font_config=font_config) + update_query = {"$set": { + 'status': 'printed' + }} + await contract.update(update_query) return FileResponse( - "out.pdf", + contract_path, media_type="application/pdf", - filename=draft.name) + filename=contract.name) + + +def retrieve_signature_png(filepath): + with open(filepath, "rb") as f: + b_content = f.read() + base64_utf8_str = base64.b64encode(b_content).decode('utf-8') + ext = filepath.split('.')[-1] + return f'data:image/{ext};base64,{base64_utf8_str}' diff --git a/back/app/contract/print/templates/print.html b/back/app/contract/print/templates/print.html index 835bd61b..0e7440fc 100644 --- a/back/app/contract/print/templates/print.html +++ b/back/app/contract/print/templates/print.html @@ -60,7 +60,14 @@

(Signatures précédée de la mention « Lu et approuvé »)

- {% for party in contract.parties %}{% endfor %} + {% for party in contract.parties %} + + {% endfor %}
{{ party.part|safe }}: + {{ party.part|safe }}:
+ {% if party.signature_png %} + + {% endif %} +
diff --git a/front/app/src/app/views/contracts/contracts.component.ts b/front/app/src/app/views/contracts/contracts.component.ts index d352418b..0b1c9ce9 100644 --- a/front/app/src/app/views/contracts/contracts.component.ts +++ b/front/app/src/app/views/contracts/contracts.component.ts @@ -1,7 +1,7 @@ -import {Component, Input, OnInit} from '@angular/core'; -import { fabric } from 'fabric'; -import {ActivatedRoute, ParamMap} from "@angular/router"; -import {DomSanitizer} from "@angular/platform-browser"; +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from "@angular/router"; +import { DomSanitizer } from "@angular/platform-browser"; +import { ImageUploaderCrudService } from "@common/crud/crud.service"; export class BaseContractsComponent { @@ -24,9 +24,36 @@ export class ContractsNewComponent extends BaseContractsComponent { } @Component({ - templateUrl: '../base-view/templates/card.template.html' + template:` + +
+ {{ this.contractPrintLink! }}  + +
+ + + `, }) export class ContractsCardComponent extends BaseContractsComponent{ + resource_id: string | null = null; + contractPrintLink: 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}` + }) + } + } } @Component({ @@ -47,7 +74,17 @@ export class ContractsCardComponent extends BaseContractsComponent{ Signature - + This Contract has already been signed by {{ this.signatory }} +
+ +
+

Cette page est à la destination exclusive de {{ this.signatory }}

+

Si vous n'êtes pas {{ this.signatory }}, veuillez fermer cette page immédiatement et surpprimer tous les liens en votre possession menant vers celle-ci.

+

En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour usurpation d'identité et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.

+

Le cabinet Cooper, Hillman & Toshi LLC

+
+
@@ -55,18 +92,41 @@ export class ContractsCardComponent extends BaseContractsComponent{ }) export class ContractsSignatureComponent implements OnInit { signature_id: string | null = null; + signature: any = {} + signatory = "" + + affixed = false; constructor( - private route: ActivatedRoute, - private sanitizer: DomSanitizer + private route: ActivatedRoute, + private sanitizer: DomSanitizer, + private crudService: ImageUploaderCrudService, ) {} - ngOnInit() {this.route.paramMap.subscribe((params: ParamMap) => { - this.signature_id = params.get('id') - }) + ngOnInit() { + this.route.paramMap.subscribe((params: ParamMap) => { + this.signature_id = params.get('id'); + this.crudService.get('contract/signature', this.signature_id!).subscribe( (response:any) => { + this.signature = response; + this.affixed = this.signature.signature_affixed; + if (this.signature.representative) { + this.signatory = this.signature.representative.entity_data.firstname + " " + this.signature.representative.entity_data.lastname; + } else { + this.signatory = this.signature.entity.entity_data.firstname + " " + this.signature.entity.entity_data.lastname; + } + }) + }) } getPreview() { return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/" + this.signature_id); } + + postSignature(image: string) { + this.crudService.upload('contract/signature', this.signature_id!, image).subscribe((v: any) => { + if (v) { + this.affixed = true; + } + }) + } } diff --git a/front/app/src/app/views/contracts/contracts.module.ts b/front/app/src/app/views/contracts/contracts.module.ts index d65ca761..060cfa90 100644 --- a/front/app/src/app/views/contracts/contracts.module.ts +++ b/front/app/src/app/views/contracts/contracts.module.ts @@ -7,12 +7,13 @@ import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent, DraftsNew import { FormlyModule } from "@ngx-formly/core"; import { FormlyBootstrapModule } from "@ngx-formly/bootstrap"; import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type"; -import { CrudService } from "@common/crud/crud.service"; +import { CrudService, ImageUploaderCrudService } from "@common/crud/crud.service"; import { NgbAccordionModule, NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap"; import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons"; import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent } from "../contracts/contracts.component"; import { AlphaRangeComponent, BlackBlueRangeComponent, SignatureDrawerComponent } from "./signature-drawer/signature-drawer.component"; +import { ClipboardModule } from "@angular/cdk/clipboard"; @NgModule({ @@ -29,6 +30,7 @@ import { AlphaRangeComponent, BlackBlueRangeComponent, SignatureDrawerComponent ] }), FormlyBootstrapModule, + ClipboardModule, ], declarations: [ DraftsListComponent, @@ -43,7 +45,7 @@ import { AlphaRangeComponent, BlackBlueRangeComponent, SignatureDrawerComponent BlackBlueRangeComponent, AlphaRangeComponent ], - providers: [CrudService] + providers: [CrudService, ImageUploaderCrudService] }) export class ContractsModule { } diff --git a/front/app/src/app/views/contracts/signature-drawer/signature-drawer.component.html b/front/app/src/app/views/contracts/signature-drawer/signature-drawer.component.html index bec78b6d..a40481a2 100644 --- a/front/app/src/app/views/contracts/signature-drawer/signature-drawer.component.html +++ b/front/app/src/app/views/contracts/signature-drawer/signature-drawer.component.html @@ -1,8 +1,8 @@
- -
+ +
@@ -13,7 +13,6 @@
-
- +