aemo_fr/aemo/pdf.py

637 lines
25 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)