Draft for contract printing
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
FROM python:3.10
|
||||
|
||||
# make the 'app' folder the current working directory
|
||||
RUN apt update && apt install -y xfonts-base xfonts-75dpi \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.bullseye_amd64.deb \
|
||||
&& dpkg -i wkhtmltox_0.12.6.1-2.bullseye_amd64.deb \
|
||||
&& rm wkhtmltox_0.12.6.1-2.bullseye_amd64.deb
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# copy both 'package.json' and 'package-lock.json' (if available)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
155
back/app/contract/print/__init__.py
Normal file
155
back/app/contract/print/__init__.py
Normal 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)
|
||||
152
back/app/contract/print/pdf_generator.py
Normal file
152
back/app/contract/print/pdf_generator.py
Normal 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)
|
||||
30
back/app/contract/print/templates/content.html
Normal file
30
back/app/contract/print/templates/content.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
{% include 'styles.css' %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h2>Conditions générales & particuliè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>
|
||||
12
back/app/contract/print/templates/footer.html
Normal file
12
back/app/contract/print/templates/footer.html
Normal 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>
|
||||
48
back/app/contract/print/templates/frontpage.html
Normal file
48
back/app/contract/print/templates/frontpage.html
Normal 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 }} à {{ draft.location}}</p>
|
||||
<p>Entre les soussigné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été de {{ party.entity.entity_data.activity }} enregistrée auprès du gouvernement de San Andreas et domiciliée au {{ party.entity.address }}{% if party.representative %}, représenté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é le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if true %} à {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
|
||||
{% if party.entity.address %} résidant à {{ party.entity.address }}, {% endif %}
|
||||
{% elif party.entity.entity_data.type == "institution" %}
|
||||
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>Ci-après dénommé <strong>{{ party.part|safe }}</strong></p>
|
||||
{% if loop.first %}
|
||||
<p class="part">d'une part</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="part">d'autre part</p>
|
||||
<p>Sous la supervision légale de Maître <strong>{{ lawyer.firstname }} {{ lawyer.lastname }}</strong></p>
|
||||
<p>Il a été convenu l'exécution des prestations ci-dessous, conformément aux conditions générales et particulières ci-après:</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
57
back/app/contract/print/templates/print.html
Normal file
57
back/app/contract/print/templates/print.html
Normal 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% à %LIEU_CONTRAT%</p>
|
||||
<p>Entre les soussigné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étéde {{ party.entity.entity_data.activity }} enregistrée auprès du gouvernement de San Andreas et domiciliée au {{ party.entity.address }} représenté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é le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if true %} à %BIRTHPLACE% {% endif %},{% endif %}
|
||||
{% if party.entity.address %} résidant à {{ party.entity.address }}, {% endif %}
|
||||
{% elif party.entity.entity_data.type == "institution" %}
|
||||
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>Ci-après dénommé <strong>{{ party.part|safe }}</strong></p>
|
||||
{% if loop.first %}
|
||||
<p class="part">d'une part</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="part">d'autre part</p>
|
||||
<p>Sous la supervision légale de Maître <strong>{{ lawyer.firstname }} {{ lawyer.lastname }}</strong></p>
|
||||
<p>Il a été convenu l'exécution des prestations ci-dessous, conformément aux conditions générales et particulières ci-après:</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Conditions générales & particuliè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>
|
||||
124
back/app/contract/print/templates/styles.css
Normal file
124
back/app/contract/print/templates/styles.css
Normal 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;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class Individual(EntityType):
|
||||
lastname: Indexed(str)
|
||||
surnames: List[Indexed(str)] = []
|
||||
day_of_birth: date
|
||||
place_of_birth: str = ""
|
||||
|
||||
|
||||
@property
|
||||
|
||||
@@ -3,4 +3,8 @@ fastapi_users==10.2.1
|
||||
fastapi_users_db_beanie==1.1.2
|
||||
motor==3.1.1
|
||||
fastapi-paginate==0.1.0
|
||||
uvicorn
|
||||
uvicorn
|
||||
jinja2
|
||||
pdfkit
|
||||
PyPDF2
|
||||
weasyprint
|
||||
|
||||
Reference in New Issue
Block a user