aemo_fr/aemo/pdf.py

637 lines
25 KiB
Python
Raw Permalink Normal View History

2024-06-03 16:49:01 +02:00
from io import BytesIO
import extract_msg
import nh3
from bs4 import BeautifulSoup
from pypdf import PdfWriter
from functools import partial
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.pdfgen import canvas
from reportlab.platypus import (
PageBreak, Paragraph, Preformatted, SimpleDocTemplate, Spacer, Table, TableStyle
)
from django.contrib.staticfiles.finders import find
from django.utils.text import slugify
from .utils import format_d_m_Y, format_duree, format_Ymd
def format_booleen(val):
return '?' if val is None else ('oui' if val else 'non')
class PageNumCanvas(canvas.Canvas):
"""A special canvas to be able to draw the total page number in the footer."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._pages = []
def showPage(self):
self._pages.append(dict(self.__dict__))
self._startPage()
def save(self):
page_count = len(self._pages)
for page in self._pages:
self.__dict__.update(page)
self.draw_page_number(page_count)
canvas.Canvas.showPage(self)
super().save()
def draw_page_number(self, page_count):
self.setFont("Helvetica", 9)
self.drawRightString(self._pagesize[0] - 1.6*cm, 2.3*cm, "p. %s/%s" % (self._pageNumber, page_count))
class CleanParagraph(Paragraph):
"""If the (HTML) text cannot be parsed, try to clean it."""
def __init__(self, text, *args, **kwargs):
if text:
text = text.replace('</p>', '</p><br/>')
if '<ul>' in text:
text = text.replace('<li>', '&nbsp;&nbsp;&bull; ').replace('</ul>','<br>').replace('</li>', '<br>')
try:
super().__init__(text, *args, **kwargs)
except ValueError:
text = nh3.clean(
text, tags={'p', 'br', 'b', 'strong', 'u', 'i', 'em', 'ul', 'li'}
).replace('<br>', '<br/>')
super().__init__(text, *args, **kwargs)
class RawParagraph(Paragraph):
"""Raw text, replace new lines by <br/>."""
def __init__(self, text='', *args, **kwargs):
if text:
text = text.replace('\r\n', '\n').replace('\n', '\n<br/>')
super().__init__(text, *args, **kwargs)
class StyleMixin:
FONTSIZE = 9
MAXLINELENGTH = 120
def __init__(self, base_font='Helvetica', **kwargs):
self.styles = getSampleStyleSheet()
self.style_title = ParagraphStyle(
name='title', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 4,
leading=self.FONTSIZE + 5, alignment=TA_CENTER
)
self.style_normal = ParagraphStyle(
name='normal', fontName='Helvetica', fontSize=self.FONTSIZE, alignment=TA_LEFT,
leading=self.FONTSIZE + 1, spaceAfter=0
)
self.style_justifie = ParagraphStyle(
name='justifie', parent=self.style_normal, alignment=TA_JUSTIFY, spaceAfter=0.2 * cm
)
self.style_sub_title = ParagraphStyle(
name='sous_titre', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 2,
alignment=TA_LEFT, spaceBefore=0.5 * cm, spaceAfter=0.1 * cm
)
self.style_inter_title = ParagraphStyle(
name='inter_titre', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 1,
alignment=TA_LEFT, spaceBefore=0.3 * cm, spaceAfter=0
)
self.style_bold = ParagraphStyle(
name='bold', fontName='Helvetica-Bold', fontSize=self.FONTSIZE, leading=self.FONTSIZE + 1
)
self.style_italic = ParagraphStyle(
name='italic', fontName='Helvetica-Oblique', fontSize=self.FONTSIZE - 1, leading=self.FONTSIZE
)
self.style_indent = ParagraphStyle(
name='indent', fontName='Helvetica', fontSize=self.FONTSIZE, alignment=TA_LEFT,
leftIndent=1 * cm
)
super().__init__(**kwargs)
class HeaderFooterMixin:
LOGO = find('img/logo-cr.png')
EDUQUA = find('img/eduqua.png')
DON = find('img/logo-zewo.png')
def draw_header(self, canvas, doc):
canvas.saveState()
canvas.drawImage(
self.LOGO, doc.leftMargin + 316, doc.height+60, 7 * cm, 1.6 * cm, preserveAspectRatio=True, mask='auto'
)
canvas.restoreState()
def draw_footer(self, canvas, doc):
canvas.saveState()
canvas.drawImage(
self.EDUQUA, doc.leftMargin, doc.height - 670, 1.8 * cm, 0.8 * cm, preserveAspectRatio=True
)
canvas.drawImage(
self.DON, doc.leftMargin + 60, doc.height - 670, 2.5 * cm, 0.8 * cm, preserveAspectRatio=True, mask='auto'
)
tab = [220, 365]
line = [658, 667, 676, 685]
canvas.setFont("Helvetica", 8)
canvas.drawRightString(doc.leftMargin + tab[0], doc.height - line[2], "CCP ?")
canvas.drawRightString(doc.leftMargin + tab[0], doc.height - line[3], "IBAN ?")
canvas.setLineWidth(0.5)
canvas.line(doc.leftMargin + 230, 2.2 * cm, doc.leftMargin + 230, 1.0 * cm)
canvas.drawRightString(doc.leftMargin + tab[1], doc.height - line[0], "Rte dEnglisberg 3")
canvas.setFont("Helvetica-Bold", 8)
canvas.drawRightString(doc.leftMargin + tab[1], doc.height - line[1], "1763 Granges-Paccot")
canvas.setFont("Helvetica", 8)
canvas.line(doc.leftMargin + 375, 2.2 * cm, doc.leftMargin + 375, 1.0 * cm)
canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[0], "+41 26 407 70 44")
canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[1], "secretariat@fondation-transit.ch")
canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[2], "fondation-transit.ch/aemo")
canvas.restoreState()
class BasePDF(HeaderFooterMixin, StyleMixin):
def __init__(self, tampon, instance, **kwargs):
self.instance = instance
self.kwargs = kwargs
self.doc = SimpleDocTemplate(
tampon, title=self.title, pagesize=A4,
leftMargin=1.5 * cm, rightMargin=1.5 * cm, topMargin=2 * cm, bottomMargin=2.5 * cm
)
self.story = []
super().__init__(**kwargs)
def draw_header_footer(self, canvas, doc):
self.draw_header(canvas, doc)
self.draw_footer(canvas, doc)
def produce(self):
# subclass should call self.doc.build(self.story, onFirstPage=self.draw_header_footer)
raise NotImplementedError
def get_filename(self):
raise NotImplementedError
@staticmethod
def format_note(note, length=StyleMixin.MAXLINELENGTH):
return Preformatted(note.replace('\r\n', '\n'), maxLineLength=length)
def set_title(self, title=''):
self.story.append(Spacer(0, 1 * cm))
self.story.append(Paragraph(title, self.style_title))
self.story.append(Spacer(0, 1.2 * cm))
def parent_data(self, person):
"""Return parent data ready to be used in a 2-column table."""
parents = person.parents()
par1 = parents[0] if len(parents) > 0 else None
par2 = parents[1] if len(parents) > 1 else None
data = [
[('Parent 1 (%s)' % par1.role.nom) if par1 else '-',
('Parent 2 (%s)' % par2.role.nom) if par2 else '-'],
[par1.contact.nom_prenom if par1 else '', par2.contact.nom_prenom if par2 else ''],
[par1.contact.adresse if par1 else '', par2.contact.adresse if par2 else ''],
[par1.contact.contact if par1 else '', par2.contact.contact if par2 else ''],
['Autorité parentale: {}'.format(format_booleen(par1.contact.autorite_parentale)) if par1 else '',
'Autorité parentale: {}'.format(format_booleen(par2.contact.autorite_parentale)) if par2 else ''],
]
return data
def formate_persons(self, parents_list):
labels = (
("Nom", "nom"), ("Prénom", "prenom"), ("Adresse", "rue"),
("Localité", 'localite_display'), ("Profession", 'profession'), ("Tél.", 'telephone'),
("Courriel", 'email'), ('Remarque', 'remarque'),
)
P = partial(Paragraph, style=self.style_normal)
Pbold = partial(Paragraph, style=self.style_bold)
data = []
parents = [parent for parent in parents_list if parent is not None]
if len(parents) == 0:
pass
elif len(parents) == 1:
data.append([Pbold('Rôle:'), Pbold(parents[0].role.nom), '', ''])
for label in labels:
data.append([
label[0], P(getattr(parents[0], label[1])), '', ''
])
elif len(parents) == 2:
data.append([
Pbold('Rôle:'), Pbold(parents[0].role.nom),
Pbold('Rôle:'), Pbold(parents[1].role.nom)
])
for label in labels:
data.append([
label[0], P(getattr(parents[0], label[1]) if parents[0] else ''),
label[0], P(getattr(parents[1], label[1]) if parents[1] else '')
])
return data
def get_table(self, data, columns, before=0.0, after=0.0, inter=None):
"""Prepare a Table instance with data and columns, with a common style."""
if inter:
inter = inter * cm
cols = [c * cm for c in columns]
t = Table(
data=data, colWidths=cols, hAlign=TA_LEFT,
spaceBefore=before * cm, spaceAfter=after * cm
)
t.hAlign = 0
t.setStyle(tblstyle=TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTSIZE', (0, 0), (-1, -1), self.FONTSIZE),
('VALIGN', (0, 0), (-1, -1), "TOP"),
('LEFTPADDING', (0, 0), (0, -1), 1),
('LEADING', (0, 0), (-1, -1), 7),
]))
return t
def _add_subtitle(self, data):
t = Table(
data=[data], colWidths=[18 * cm / len(data)] * len(data),
hAlign=TA_LEFT,
spaceBefore=0.2 * cm, spaceAfter=0.5 * cm
)
t.hAlign = 0
t.setStyle(tblstyle=TableStyle([
('FONT', (0, 0), (-1, -1), "Helvetica-Bold"),
('FONTSIZE', (0, 0), (-1, -1), self.FONTSIZE + 2),
('LINEBELOW', (0, -1), (-1, -1), 0.25, colors.black),
('ALIGN', (-1, -1), (-1, -1), 'RIGHT'),
]))
self.story.append(t)
def write_paragraph(self, title, text, html=False, justifie=False):
if title:
self.story.append(Paragraph(title, self.style_sub_title))
style = self.style_justifie if justifie else self.style_normal
if html:
if text.startswith('<p>') and text.endswith('</p>'):
soup = BeautifulSoup(text, features="html5lib")
for tag in soup.find_all(['p', 'ul']):
self.story.append(CleanParagraph(str(tag), style))
else:
self.story.append(CleanParagraph(text, style))
else:
self.story.append(RawParagraph(text, style))
def enfant_data(self, enfant):
labels = [('Tél.', enfant.telephone)]
if hasattr(enfant, 'formation'):
labels.extend([
('Statut scol', enfant.formation.get_statut_display()),
('Centre', enfant.formation.cercle_scolaire),
('Collège', enfant.formation.college),
('Classe', enfant.formation.classe),
('Struct. extra-fam.', enfant.formation.creche),
('Ens.', enfant.formation.enseignant),
])
labels.extend([
('Permis de séjour', enfant.permis),
('Validité', enfant.validite),
])
row = [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"]
for label in labels:
if label[1]:
row.append(f"{label[0]}: {label[1]}")
return '; '.join(row)
class DemandeAccompagnement(BasePDF):
title = None
def produce(self):
famille = self.instance
suivi = famille.suivi
self.set_title("Famille {} - {}".format(famille.nom, self.title))
self.story.append(Paragraph("<strong>Motif(s) de la demande:</strong> {}".format(
suivi.get_motif_demande_display()), self.style_normal
))
self.story.append(Paragraph('_' * 90, self.style_normal))
self.write_paragraph("Dates", suivi.dates_demande)
self.write_paragraph(
"Difficultés",
"{}<br/>{}<br/><br/>{}".format(
"<em>Quelles sont les difficultés éducatives que vous rencontrez et depuis combien de "
"temps ?",
"Fonctionnement familial: règles, coucher, lever, repas, jeux, relations "
"parent-enfants, rapport au sein de la fratrie, … (exemple)</em>",
suivi.difficultes
),
html=True
)
self.write_paragraph(
"Autres services",
"<em>{}</em><br/>{}".format(
"Avez-vous fait appel à d'autres services ? Si oui, avec quels vécus ?",
suivi.autres_contacts
),
html=True
)
self.write_paragraph("Aides souhaitées", suivi.aides, html=True)
self.write_paragraph("Ressources/Compétences", suivi.competences, html=True)
self.write_paragraph("Disponibilités", suivi.disponibilites, html=True)
self.write_paragraph("Remarques", suivi.remarque)
self.doc.build(self.story, onFirstPage=self.draw_header_footer)
class JournalPdf(BasePDF):
title = "Journal de bord"
def get_filename(self):
return '{}_journal.pdf'.format(slugify(self.instance.nom))
def get_title(self):
return 'Famille {} - {}'.format(self.instance.nom, self.title)
def produce(self):
famille = self.instance
self.set_title(self.get_title())
self.style_bold.spaceAfter = 0.2*cm
self.style_italic.spaceBefore = 0.2 * cm
self.style_italic.spaceAfter = 0.7 * cm
for prest in famille.prestations.all().prefetch_related('intervenants'):
self.story.append(CleanParagraph(prest.texte, self.style_normal))
self.story.append(
Paragraph('{} - {} ({})'.format(
'/'.join(interv.sigle for interv in prest.intervenants.all()),
format_d_m_Y(prest.date_prestation),
format_duree(prest.duree)
), self.style_italic)
)
self.doc.build(
self.story,
onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer,
canvasmaker=PageNumCanvas
)
class RapportPdf(BasePDF):
title = None
def get_filename(self):
return "{}_resume_{}.pdf".format(
slugify(self.instance.famille.nom),
format_Ymd(self.instance.date)
)
def produce(self):
rapport = self.instance
self.style_normal.fontSize += 2
self.style_normal.leading += 2
self.style_justifie.fontSize += 2
self.style_justifie.leading += 2
self.doc.title = 'Résumé "AEMO"'
self.set_title('Famille {} - {}'.format(rapport.famille.nom, self.doc.title))
self._add_subtitle([f"Date: {format_d_m_Y(rapport.date)}"])
data = [
("Enfant(s)", '<br/>'.join(
[f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"
for enfant in rapport.famille.membres_suivis()]), False),
("Intervenant-e-s", ', '.join([i.nom_prenom for i in rapport.intervenants()]), False),
("Début du suivi", format_d_m_Y(rapport.famille.suivi.date_debut_suivi), False),
("Situation / contexte familial", rapport.situation, True),
]
data.append(("Observations", rapport.observations, True))
data.append(("Perspectives d'avenir", rapport.projet, True))
for title, text, html in data:
self.write_paragraph(title, text, html=html, justifie=True)
if hasattr(rapport, 'sig_interv'):
self.story.append(Spacer(0, 0.5 * cm))
for idx, interv in enumerate(rapport.sig_interv.all()):
if idx == 0:
self.write_paragraph("Signature des intervenant-e-s :", '')
self.story.append(Spacer(0, 0.2 * cm))
self.write_paragraph('', interv.nom_prenom + (f', {interv.profession}' if interv.profession else ''))
self.story.append(Spacer(0, 1 * cm))
secret_style = ParagraphStyle(
name='italic', fontName='Helvetica-Oblique', fontSize=self.FONTSIZE - 1, leading=self.FONTSIZE + 1,
backColor=colors.Color(0.96, 0.96, 0.96, 1), borderRadius=12,
)
self.story.append(Paragraph(
"Le présent résumé comporte des éléments <b>couverts par le secret professionnel au sens "
"de la LPSy et du Code pénal</b>. Seuls les propriétaires des données, à savoir les membres "
"de la famille faisant lobjet du résumé, peuvent <b>ensemble</b> lever ce secret ou "
"accepter la divulgation des données. Si cette autorisation nest pas donnée, lautorité "
"compétente en matière de levée du secret professionnel doit impérativement être saisie.",
secret_style
))
self.doc.build(self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer)
class MessagePdf(BasePDF):
title = 'Message'
def get_filename(self):
return '{}_message.pdf'.format(slugify(self.instance.subject))
def produce(self):
doc = self.instance
self.set_title('{} - Famille {}'.format(self.title, doc.famille.nom))
with extract_msg.Message(doc.fichier.path) as msg:
P = partial(Paragraph, style=self.style_normal)
Pbold = partial(Paragraph, style=self.style_bold)
msg_headers = [
[Pbold('De:'), P(msg.sender)],
[Pbold('À:'), P(msg.to)],
]
if msg.cc:
msg_headers.append([Pbold('CC:'), P(msg.cc)])
if msg.date:
msg_headers.append([Pbold('Date:'), P(msg.date)])
msg_headers.append([Pbold('Sujet:'), P(msg.subject)])
self.story.append(self.get_table(msg_headers, [3, 15]))
self.story.append(Pbold('Message:'))
self.story.append(RawParagraph(msg.body, style=self.style_normal))
return self.doc.build(
self.story,
onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer,
canvasmaker=PageNumCanvas
)
class EvaluationPdf:
def __init__(self, tampon, famille):
self.tampon = tampon
self.famille = famille
self.merger = PdfWriter()
def append_pdf(self, PDFClass):
tampon = BytesIO()
pdf = PDFClass(tampon, self.famille)
pdf.produce()
self.merger.append(tampon)
def produce(self):
self.append_pdf(CoordonneesPagePdf)
self.append_pdf(DemandeAccompagnementPagePdf)
self.merger.write(self.tampon)
def get_filename(self):
return '{}_aemo_evaluation.pdf'.format(slugify(self.famille.nom))
class CoordonneesFamillePdf(BasePDF):
title = "Informations"
def get_filename(self):
return '{}_coordonnees.pdf'.format(slugify(self.instance.nom))
def produce(self):
famille = self.instance
suivi = famille.suivi
self.set_title('Famille {} - {}'.format(famille.nom, self.title))
# Parents
self.story.append(Paragraph('Parents', self.style_sub_title))
data = self.formate_persons(famille.parents())
if data:
self.story.append(self.get_table(data, columns=[2, 7, 2, 7], before=0, after=0))
else:
self.story.append(Paragraph('-', self.style_normal))
# Situation matrimoniale
data = [
['Situation matrimoniale: {}'.format(famille.get_statut_marital_display()),
'Autorité parentale: {}'.format(famille.get_autorite_parentale_display())],
]
self.story.append(self.get_table(data, columns=[9, 9], before=0, after=0))
# Personnes significatives
autres_parents = list(famille.autres_parents())
if autres_parents:
self.story.append(Paragraph('Personne-s significative-s', self.style_sub_title))
data = self.formate_persons(autres_parents)
self.story.append(self.get_table(data, columns=[2, 7, 3, 6], before=0, after=0))
if len(autres_parents) > 2:
self.story.append(PageBreak())
# Enfants suivis
self.write_paragraph(
"Enfant(s)",
'<br/>'.join(self.enfant_data(enfant) for enfant in famille.membres_suivis())
)
# Réseau
self.story.append(Paragraph("Réseau", self.style_sub_title))
data = [
['AS OPE', '{} - (Mandat: {})'.format(
', '.join(ope.nom_prenom for ope in suivi.ope_referents),
suivi.get_mandat_ope_display()
)],
['Interv. CRNE', '{}'.format(
', '.join('{}'.format(i.nom_prenom) for i in suivi.intervenants.all().distinct())
)]
]
for enfant in famille.membres_suivis():
for contact in enfant.reseaux.all():
data.append([
enfant.prenom,
'{} ({})'.format(contact, contact.contact)
])
self.story.append(self.get_table(data, columns=[2, 16], before=0, after=0))
self.write_paragraph("Motif de la demande", famille.suivi.motif_detail)
self.write_paragraph("Collaborations", famille.suivi.collaboration)
# Historique
self.story.append(Paragraph('Historique', self.style_sub_title))
P = partial(Paragraph, style=self.style_normal)
Pbold = partial(Paragraph, style=self.style_bold)
fields = ['date_demande', 'date_debut_evaluation', 'date_fin_evaluation',
'date_debut_suivi', 'date_fin_suivi']
data = []
for field_name in fields:
field = famille.suivi._meta.get_field(field_name)
if getattr(famille.suivi, field_name):
data.append(
[Pbold(f"{field.verbose_name} :"), P(format_d_m_Y(getattr(famille.suivi, field_name)))]
)
if famille.suivi.motif_fin_suivi:
data.append([Pbold("Motif de fin de suivi :"), famille.suivi.get_motif_fin_suivi_display()])
if famille.destination:
data.append([Pbold("Destination :"), famille.get_destination_display()])
if famille.archived_at:
data.append([Pbold("Date d'archivage :"), format_d_m_Y(famille.archived_at)])
self.story.append(self.get_table(data, [4, 5]))
self.doc.build(self.story, onFirstPage=self.draw_header_footer)
class DemandeAccompagnementPagePdf(DemandeAccompagnement):
title = "Évaluation AEMO"
class BilanPdf(BasePDF):
title = "Bilan AEMO"
def get_filename(self):
return "{}_bilan_{}.pdf".format(
slugify(self.instance.famille.nom),
format_Ymd(self.instance.date)
)
def produce(self):
bilan = self.instance
self.style_normal.fontSize += 2
self.style_normal.leading += 2
self.style_justifie.fontSize += 2
self.style_justifie.leading += 2
self.set_title('Famille {} - {}'.format(bilan.famille.nom, self.title))
self._add_subtitle([f"Date: {format_d_m_Y(bilan.date)}"])
for title, text, html in (
("Enfant(s)", '<br/>'.join(
[f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"
for enfant in bilan.famille.membres_suivis()]), False),
("Intervenant-e-s", ', '.join(
[i.intervenant.nom_prenom for i in bilan.famille.interventions_actives(bilan.date)]
), False),
("Début du suivi", format_d_m_Y(bilan.famille.suivi.date_debut_suivi), False),
("Besoins et objectifs", bilan.objectifs, True),
("Rythme et fréquence", bilan.rythme, True),
):
self.write_paragraph(title, text, html=html, justifie=True)
self.story.append(Spacer(0, 0.5 * cm))
for idx, interv in enumerate(bilan.sig_interv.all()):
if idx == 0:
self.write_paragraph("Signature des intervenant-e-s AEMO :", '')
self.story.append(Spacer(0, 0.2 * cm))
self.write_paragraph('', interv.nom_prenom + (f', {interv.profession}' if interv.profession else ''))
self.story.append(Spacer(0, 1 * cm))
if bilan.sig_famille:
self.write_paragraph("Signature de la famille :", '')
self.doc.build(self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer)
class CoordonneesPagePdf(CoordonneesFamillePdf):
title = "Informations"
def get_filename(self, famille):
return '{}_aemo_evaluation.pdf'.format(slugify(famille.nom))
def str_or_empty(value):
return '' if not value else str(value)