Draft for contract printing

This commit is contained in:
2023-02-17 19:46:22 +01:00
parent 749794a5f8
commit 7ac89f1bb3
15 changed files with 592 additions and 4 deletions

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter
from .routes_draft import draft_router
from .routes_print import print_router
from .print import print_router
contract_router = APIRouter()

View File

@@ -0,0 +1,155 @@
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates
import pdfkit
from PyPDF2 import PdfMerger
from pathlib import Path
from app.entity.models import Entity
from app.template.models import ProvisionTemplate
from ..schemas import ContractDraft
async def build_model(model):
parties = []
for p in model.parties:
party = {
"entity": await Entity.get(p.entity_id),
"part": p.part
}
if p.representative_id:
party['representative'] = await Entity.get(p.representative_id)
parties.append(party)
model.parties = parties
provisions = []
for p in model.provisions:
if p.provision.type == "template":
provisions.append(await ProvisionTemplate.get(p.provision.provision_template_id))
else:
provisions.append(p.provision)
model.provisions = provisions
model.location = "Toulouse"
model.date = "01/01/1970"
return model
BASE_PATH = Path(__file__).resolve().parent
print_router = APIRouter()
templates = Jinja2Templates(directory=str(BASE_PATH / "templates"))
options = {
'encoding': 'UTF-8',
'margin-left': '20mm',
'margin-right': '20mm',
'margin-bottom': '20mm',
'margin-top': '20mm'
}
options_content = options.copy()
options_content['footer-html'] = 'footer.html'
options_content['margin-bottom'] = '60mm'
async def render_template(host):
draft = await ContractDraft.get("63e92534aafed8b509f229c4")
lawyer = {
"firstname": "Nathaniel",
"lastname": "Toshi",
}
template = templates.get_template("print.html")
return template.render({
"draft": await build_model(draft),
"lawyer": lawyer,
"static_host": host
})
async def render_frontpage(host, draft, lawyer, ):
template = templates.get_template("frontpage.html")
return template.render({
"draft": draft,
"lawyer": lawyer,
"static_host": host
})
async def render_content(host, draft, ):
template = templates.get_template("content.html")
return template.render({
"draft": draft,
"static_host": host
})
async def render_footer(host, draft, ):
template = templates.get_template("footer.html")
return template.render({
"draft": draft,
"static_host": host
})
@print_router.get("/frontpage", response_class=HTMLResponse)
async def frontpage() -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
lawyer = {
"firstname": "Nathaniel",
"lastname": "Toshi",
}
return await render_frontpage('localhost', draft, lawyer)
@print_router.get("/content", response_class=HTMLResponse)
async def content() -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
return await render_content('localhost', draft)
@print_router.get("/footer", response_class=HTMLResponse)
async def footer() -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
return await render_footer('localhost', draft)
@print_router.get("/", response_class=HTMLResponse)
async def create() -> str:
return await render_template('localhost')
@print_router.get("/pdf", response_class=FileResponse)
async def create_pdf() -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
lawyer = {
"firstname": "Nathaniel",
"lastname": "Toshi",
}
merger = PdfMerger()
pdfkit.from_string(await render_frontpage('nginx', draft, lawyer), 'front.pdf', options=options)
merger.append('front.pdf')
with open('footer.html', 'w') as f:
f.write(await render_footer('nginx', draft))
pdfkit.from_string(await render_content('nginx', draft), 'content.pdf', options=options_content)
merger.append('content.pdf')
merger.write("out.pdf")
merger.close()
print(options)
return FileResponse(
"out.pdf",
media_type="application/pdf",
filename=draft.name)

View File

@@ -0,0 +1,152 @@
from weasyprint import HTML, CSS
class PdfGenerator:
"""
Generate a PDF out of a rendered template, with the possibility to integrate nicely
a header and a footer if provided.
Notes:
------
- When Weasyprint renders an html into a PDF, it goes though several intermediate steps.
Here, in this class, we deal mostly with a box representation: 1 `Document` have 1 `Page`
or more, each `Page` 1 `Box` or more. Each box can contain other box. Hence the recursive
method `get_element` for example.
For more, see:
https://weasyprint.readthedocs.io/en/stable/hacking.html#dive-into-the-source
https://weasyprint.readthedocs.io/en/stable/hacking.html#formatting-structure
- Warning: the logic of this class relies heavily on the internal Weasyprint API. This
snippet was written at the time of the release 47, it might break in the future.
- This generator draws its inspiration and, also a bit of its implementation, from this
discussion in the library github issues: https://github.com/Kozea/WeasyPrint/issues/92
"""
OVERLAY_LAYOUT = '@page {size: A4 portrait; margin: 0;}'
def __init__(self, main_html, header_html=None, footer_html=None,
base_url=None, side_margin=2, extra_vertical_margin=30):
"""
Parameters
----------
main_html: str
An HTML file (most of the time a template rendered into a string) which represents
the core of the PDF to generate.
header_html: str
An optional header html.
footer_html: str
An optional footer html.
base_url: str
An absolute url to the page which serves as a reference to Weasyprint to fetch assets,
required to get our media.
side_margin: int, interpreted in cm, by default 2cm
The margin to apply on the core of the rendered PDF (i.e. main_html).
extra_vertical_margin: int, interpreted in pixel, by default 30 pixels
An extra margin to apply between the main content and header and the footer.
The goal is to avoid having the content of `main_html` touching the header or the
footer.
"""
self.main_html = main_html
self.header_html = header_html
self.footer_html = footer_html
self.base_url = base_url
self.side_margin = side_margin
self.extra_vertical_margin = extra_vertical_margin
def _compute_overlay_element(self, element: str):
"""
Parameters
----------
element: str
Either 'header' or 'footer'
Returns
-------
element_body: BlockBox
A Weasyprint pre-rendered representation of an html element
element_height: float
The height of this element, which will be then translated in a html height
"""
html = HTML(
string=getattr(self, f'{element}_html'),
base_url=self.base_url,
)
element_doc = html.render(stylesheets=[CSS(string=self.OVERLAY_LAYOUT)])
element_page = element_doc.pages[0]
element_body = PdfGenerator.get_element(element_page._page_box.all_children(), 'body')
element_body = element_body.copy_with_children(element_body.all_children())
element_html = PdfGenerator.get_element(element_page._page_box.all_children(), element)
if element == 'header':
element_height = element_html.height
if element == 'footer':
element_height = element_page.height - element_html.position_y
return element_body, element_height
def _apply_overlay_on_main(self, main_doc, header_body=None, footer_body=None):
"""
Insert the header and the footer in the main document.
Parameters
----------
main_doc: Document
The top level representation for a PDF page in Weasyprint.
header_body: BlockBox
A representation for an html element in Weasyprint.
footer_body: BlockBox
A representation for an html element in Weasyprint.
"""
for page in main_doc.pages:
page_body = PdfGenerator.get_element(page._page_box.all_children(), 'body')
if header_body:
page_body.children += header_body.all_children()
if footer_body:
page_body.children += footer_body.all_children()
def render_pdf(self):
"""
Returns
-------
pdf: a bytes sequence
The rendered PDF.
"""
if self.header_html:
header_body, header_height = self._compute_overlay_element('header')
else:
header_body, header_height = None, 0
if self.footer_html:
footer_body, footer_height = self._compute_overlay_element('footer')
else:
footer_body, footer_height = None, 0
margins = '{header_size}px {side_margin} {footer_size}px {side_margin}'.format(
header_size=header_height + self.extra_vertical_margin,
footer_size=footer_height + self.extra_vertical_margin,
side_margin=f'{self.side_margin}cm',
)
content_print_layout = '@page {size: A4 portrait; margin: %s;}' % margins
html = HTML(
string=self.main_html,
base_url=self.base_url,
)
main_doc = html.render(stylesheets=[CSS(string=content_print_layout)])
if self.header_html or self.footer_html:
self._apply_overlay_on_main(main_doc, header_body, footer_body)
pdf = main_doc.write_pdf()
return pdf
@staticmethod
def get_element(boxes, element):
"""
Given a set of boxes representing the elements of a PDF page in a DOM-like way, find the
box which is named `element`.
Look at the notes of the class for more details on Weasyprint insides.
"""
for box in boxes:
if box.element_tag == element:
return box
return PdfGenerator.get_element(box.all_children(), element)

View File

@@ -0,0 +1,30 @@
<html>
<head>
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<div class="content">
<h2>Conditions g&eacute;n&eacute;rales & particuli&egrave;res</h2>
{% for provision in draft.provisions %}
<div class="provision">
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
<p>{{ provision.body|safe }}</p>
</div>
{% endfor %}
<div class="footer">
<hr/>
<p>À {{ draft.location }} le {{ draft.date }}</p>
<p class="mention">(Signatures précédée de la mention « Lu et approuvé »)</p>
<table class="signatures">
<tr>
{% for party in draft.parties %}<td>{{ party.part|safe }}:</td>{% endfor %}
</tr>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<hr/>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<html>
<head>
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<div class="frontpage">
<div id="front-page-header">
<table><tr>
<td><img id="top-logo" src="http://{{ static_host }}/assets/logotransparent.png"></td>
<td id="office-info">Cooper, Hillman & Toshi LLP<br />6834 Innocence Boulevard<br />LOS SANTOS - SA<br /><a href="#">consulting@cht.law.com</a></td>
</tr></table>
</div>
<h1>{{ draft.title|upper }}</h1>
<div class="intro">
<h2>Introduction</h2>
<p>Le {{ draft.date }} &agrave; {{ draft.location}}</p>
<p>Entre les soussign&eacute;s :</p>
{% for party in draft.parties %}
<div class="party">
{% if not loop.first %}
<p>ET</p>
{% endif %}
<p>
{% if party.entity.entity_data.type == "corporation" %}
{{ party.entity.entity_data.title }} soci&eacute;t&eacute; de {{ party.entity.entity_data.activity }} enregistr&eacute;e aupr&egrave;s du gouvernement de San Andreas et domicili&eacute;e au {{ party.entity.address }}{% if party.representative %}, repr&eacute;sent&eacute;e par {{ party.representative.entity_data.firstname }} {{ party.representative.entity_data.middlenames }} {{ party.representative.entity_data.lastname }}{% endif %}
{% elif party.entity.entity_data.type == "individual" %}
{{ party.entity.entity_data.firstname }} {{ party.entity.entity_data.middlenames }} {{ party.entity.entity_data.lastname }}
{% if party.entity.entity_data.day_of_birth %} n&eacute; le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if true %} &agrave; {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
{% if party.entity.address %} r&eacute;sidant &agrave; {{ party.entity.address }}, {% endif %}
{% elif party.entity.entity_data.type == "institution" %}
{% endif %}
</p>
<p>Ci-apr&egrave;s d&eacute;nomm&eacute; <strong>{{ party.part|safe }}</strong></p>
{% if loop.first %}
<p class="part">d&apos;une part</p>
{% endif %}
</div>
{% endfor %}
<p class="part">d&apos;autre part</p>
<p>Sous la supervision l&eacute;gale de Ma&icirc;tre <strong>{{ lawyer.firstname }} {{ lawyer.lastname }}</strong></p>
<p>Il a &eacute;t&eacute; convenu l&apos;ex&eacute;cution des prestations ci-dessous, conform&eacute;ment aux conditions g&eacute;n&eacute;rales et particuli&egrave;res ci-apr&egrave;s:</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<html>
<head>
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<div id="front-page-header">
<table><tr>
<td><img id="top-logo" src="http://{{ static_host }}/assets/logotransparent.png"></td>
<td id="office-info">Cooper, Hillman & Toshi LLP<br />6834 Innocence Boulevard<br />LOS SANTOS - SA<br /><a href="#">consulting@cht.law.com</a></td>
</tr></table>
</div>
<h1>{{ draft.title|upper }}</h1>
<div class="intro">
<h2>Introduction</h2>
<p>Le %DATE_CONTRAT% &agrave; %LIEU_CONTRAT%</p>
<p>Entre les soussign&eacute;s :</p>
{% for party in draft.parties %}
<div class="party">
{% if not loop.first %}
<p>ET</p>
{% endif %}
<p>
{% if party.entity.entity_data.type == "corporation" %}
{{ party.entity.entity_data.title }} soci&eacute;t&eacute;de {{ party.entity.entity_data.activity }} enregistr&eacute;e aupr&egrave;s du gouvernement de San Andreas et domicili&eacute;e au {{ party.entity.address }} repr&eacute;sent&eacute;e par %NOM_REPRESENTANT%
{% elif party.entity.entity_data.type == "individual" %}
{{ party.entity.entity_data.firstname }} {{ party.entity.entity_data.middlenames }} {{ party.entity.entity_data.lastname }}
{% if party.entity.entity_data.day_of_birth %} n&eacute; le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if true %} &agrave; %BIRTHPLACE% {% endif %},{% endif %}
{% if party.entity.address %} r&eacute;sidant &agrave; {{ party.entity.address }}, {% endif %}
{% elif party.entity.entity_data.type == "institution" %}
{% endif %}
</p>
<p>Ci-apr&egrave;s d&eacute;nomm&eacute; <strong>{{ party.part|safe }}</strong></p>
{% if loop.first %}
<p class="part">d&apos;une part</p>
{% endif %}
</div>
{% endfor %}
<p class="part">d&apos;autre part</p>
<p>Sous la supervision l&eacute;gale de Ma&icirc;tre <strong>{{ lawyer.firstname }} {{ lawyer.lastname }}</strong></p>
<p>Il a &eacute;t&eacute; convenu l&apos;ex&eacute;cution des prestations ci-dessous, conform&eacute;ment aux conditions g&eacute;n&eacute;rales et particuli&egrave;res ci-apr&egrave;s:</p>
</div>
<div class="content">
<h2>Conditions g&eacute;n&eacute;rales & particuli&egrave;res</h2>
{% for provision in draft.provisions %}
<div class="provision">
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
<p>{{ provision.body|safe }}</p>
</div>
{% endfor %}
</div>
</body>
</html>

View File

@@ -0,0 +1,124 @@
@font-face {
font-family: 'Century Schoolbook';
src: url('http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookRegular.ttf');
}
@font-face {
font-family: "Century Schoolbook";
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookBold.ttf");
font-weight: bold;
}
@font-face {
font-family: "Century Schoolbook";
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookItalic.ttf");
font-style: italic;
}
@font-face {
font-family: "Century Schoolbook";
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookBoldItalic.ttf");
font-weight: bold;
font-style: italic;
}
body {
width: 21cm;
font-size: 1.2em;
}
.content, .frontpage, .footer {
font-family: 'Century Schoolbook';
}
#front-page-header table {
width: 100%
}
#top-logo {
width: 5cm;
width: 5cm;
}
#office-info {
text-align: right;
vertical-align: middle;
}
h1 {
background: black;
color: white;
text-align: center;
font-size: 2.8em;
padding: 13px 0;
margin: 100px 0;
font-weight: bold;
}
h2 {
background: lightgrey;
font-size: 1.8em;
padding: 8px 0;
font-weight: bold;
}
.intro {
page-break-inside: avoid;
}
.party {
page-break-inside: avoid;
}
.part {
text-align: right;
}
.content {
background: url('http://{{ static_host }}/assets/watermark.png') repeat-y fixed left top;
background-size:contain;
}
.content h2 {
page-break-before: always;
}
.content h3 {
margin-top: 55px;
font-weight: bold;
font-size: 1.5em;
page-break-after: avoid;
}
.content p {
page-break-inside: avoid;
text-indent: 2em;
}
.provision {
page-break-inside: avoid;
}
p {
text-align: justify;
}
.footer {
margin-top: 30px;
}
.mention {
margin: 0px;
font-size: 0.9em;
}
.signatures {
width: 100%;
}
.signatures td {
vertical-align: top;
text-align: center;
height: 3cm;
}