Ultron/ultron/tools/pdf_generator.py

737 lines
24 KiB
Python

import locale
import os
import re
from datetime import date, datetime, timedelta
from statistics import mean
import pendulum
import yaml
from django.conf import settings
from django.db.models import F, Max, Q
from PIL import Image
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph, Table, TableStyle
from ultron.followup.models import (
Plan,
Point,
Chrono,
Accident,
MindState,
HeightWeight,
LearnedSkill,
)
from ultron.followup.models import LEARNING_STEP_CHOICES
from ultron.objective.models import Skill
from ultron.people.models import Gymnast
from ultron.planning.models import Event
from .date_week_transition import from_date_to_week_number
import environ
from pathlib import Path
# Initialise environment variables
env = environ.Env()
environ.Env.read_env()
# EXPENSES = 0
# RECETTES = 1
X = 35
Y = 841.89
INDENT = 15
RIGHT_X = 595.27 - X
TITLED_X = 125
INDENTED_X = X + INDENT
INDENTED_RIGHT_X = RIGHT_X - INDENT
MIDDLE = (RIGHT_X - X) / 2
PRESTATION_COLUMN_2 = INDENTED_X + 65
PRESTATION_COLUMN_3 = INDENTED_X + 400
PRESTATION_COLUMN_4 = INDENTED_X + 455
COMMON_LINE_HEIGHT = -15
SMALL_LINE_HEIGHT = COMMON_LINE_HEIGHT + 5
LARGE_LINE_HEIGHT = COMMON_LINE_HEIGHT - 5
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
class PDFDocument(object):
# Create the PDF object, using the response object as its "file."
# http://www.reportlab.com/docs/reportlab-userguide.pdf
# canvas.rect(x, y, width, height, stroke=1, fill=0)
# localhost:8000/billing/contract/pdf/2
def __init__(self, response):
# Create the PDF object, using the response object as its "file."
self.document = Canvas(response, pagesize=A4)
self.y = Y - X
self.styles = getSampleStyleSheet()
self.style = self.styles["Normal"]
self.site_title = env("SITE_TITLE", default=None)
self.club_name = env("CLUB_NAME", default=None)
self.address = env("ADDRESS", default=None)
self.city = env("CITY", default=None)
self.zip = env("ZIP", default=None)
self.head_coach = env("HEAD_COACH", default=None)
self.mobile_phone = env("MOBILE_PHONE", default=None)
self.coach_email = env("HEAD_COACH_EMAIL", default=None)
def new_page(self):
""" """
# self.y = Y - X
self.document.showPage()
self.y = Y - X
def add_vspace(self, height=COMMON_LINE_HEIGHT):
""" Passe à la ligne, la hauteur de la ligne étant passée en paramètre.
Args:
height (int): hauteur de la ligne.
Returns:
ne retourne rien.
"""
self.y += height
# print(self.y)
# if y < 120;
# document.PageBreak()
# y = 790
def add_string(
self, x, string, font_family="Helvetica", font_decoration=None, font_size=10
):
if font_decoration:
font_family += "-" + font_decoration
self.document.setFont(font_family, font_size)
self.document.drawString(x, self.y, string)
def add_new_line(
self,
x,
string,
height=COMMON_LINE_HEIGHT,
font_family="Helvetica",
font_decoration=None,
font_size=10,
):
self.add_vspace(height)
self.add_string(x, string, font_family, font_decoration, font_size)
def add_header(self, contract=None):
""" Génère le header du document.
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien.
"""
self.document.setFillColorRGB(0.75,0.75,0.75)
self.add_vspace(15)
self.add_new_line(X, self.site_title + ' - ' + self.club_name)
self.document.drawRightString(
RIGHT_X,
self.y,
self.address
+ " - "
+ self.zip
+ " "
+ self.city,
)
self.add_new_line(
X,
"Head Coach : "
+ self.head_coach
)
self.document.drawRightString(
RIGHT_X,
self.y,
self.coach_email
)
today = pendulum.now().date()
self.add_new_line(X, today.strftime("%d %B %Y"))
begin_season = date(today.year, 9, 1)
season, week_number = from_date_to_week_number()
self.document.drawRightString(RIGHT_X, self.y, "Season " + season + " - week " + str(week_number))
self.document.setFillColorRGB(0,0,0)
self.add_vspace(BIG_LINE_HEIGHT)
def download(self):
# Close the PDF object cleanly, and we're done.
self.document.showPage()
self.document.save()
class GymnastReportDocument(PDFDocument):
def generate(self, gymnast_id):
""" Genère un document aux normes du SPF Finance.
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
gymnast = Gymnast.objects.get(pk=gymnast_id)
self.document.setTitle(gymnast.first_name + ' ' + gymnast.last_name)
self.add_header()
self.add_gymnast_personnal_information(gymnast)
self.add_gymnast_physiological_information(gymnast)
self.add_gymnast_best_scores(gymnast)
self.add_gymnast_active_routine(gymnast)
# self.add_gymnast_level_information(gymnast)
plan_list = self.add_gymnast_planned_skill(gymnast)
self.add_gymnast_last_learned_skill(gymnast)
self.add_gymnast_next_events(gymnast)
self.add_gymnast_week_notes(gymnast)
if plan_list:
self.new_page()
self.add_planned_skills_details(plan_list)
def add_gymnast_personnal_information(self, gymnast):
""" Ajoute les informations personnelles du gymnast.
Args:
gymnast <Gymnast>: gymnaste
Returns:
ne retourne rien.
"""
self.y = 26*cm
url = os.path.join(settings.STATICFILES_DIRS[0], "img/default-avatar.png")
self.document.drawImage(url, X, self.y - 65, width=80, height=80)
self.add_string(
130,
str(gymnast),
font_decoration="Bold",
font_size=14,
)
self.document.setFillColorRGB(0.75, 0.75, 0.75)
self.add_new_line(
130,
str(gymnast.age)
)
self.document.setFillColorRGB(0, 0, 0)
if gymnast.informations:
self.add_new_line(
130,
str(gymnast.informations)
)
def analyse_score(self, value, value_list):
""" Analyse une value (value) par rapport à la moyenne de value_list et à la dernière
valeur de value_list
Args:
value float valeur
value_list array<float> liste de valeurs
Returns:
string
Examples:
"""
res = ''
mean_value = mean(value_list)
if value > value_list[-1]:
res += '+'
elif value < value_list[-1]:
res += '-'
else:
res += '='
if value > mean_value:
res = '+' + res
elif value < mean_value:
res = '-' + res
else:
res = '=' + res
return res
def add_gymnast_physiological_information(self, gymnast):
""" Ajoute les informations physique et psychologique.
Args:
gymnast <Gymnast>: gymnaste
Returns:
ne retourne rien
"""
self.y = 26*cm
self.add_new_line(
13.5*cm,
"Physics/Mind state",
font_decoration="Bold",
)
data = []
mindstate_queryset = MindState.objects.filter(gymnast=gymnast).order_by('-date')
last_mindstate = mindstate_queryset.first()
lasts_mindstate = list(mindstate_queryset.values_list("score", flat=True)[1:6])
have_physiological = False
if lasts_mindstate:
res = self.analyse_score(last_mindstate.score, lasts_mindstate)
data.append(["Mind state", str(last_mindstate.score), res])
have_physiological = True
height_weight_queryset = HeightWeight.objects.filter(gymnast=gymnast).order_by('-date')
last_height_weigth = height_weight_queryset.first()
lasts_height = list(height_weight_queryset.values_list("height", flat=True)[1:6])
lasts_weight = list(height_weight_queryset.values_list("weight", flat=True)[1:6])
if lasts_height:
res = self.analyse_score(last_height_weigth.height, lasts_height)
data.append(["Height", str(last_height_weigth.height), res])
have_physiological = True
if lasts_weight:
res = self.analyse_score(last_height_weigth.weight, lasts_weight)
data.append(["Weight", str(last_height_weigth.weight), res])
have_physiological = True
if have_physiological:
style = TableStyle(
[
('ALIGN', (1,0), (-1,-1), 'RIGHT'),
# ('GRID', (0,0), (-1,-1), 0.25, colors.black),
# ('BOX', (0,0), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2*cm, 1.5*cm, 1.5*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15.5*cm)
table.drawOn(self.document, 13.3*cm, self.y - height - 5)
else:
self.add_new_line(
13.5*cm,
"No recorded data.",
)
def add_gymnast_best_scores(self, gymnast):
""" Ajoute les meilleurs scores du gymnaste (Tof, compétition, …).
Args:
gymnast <Gymnast>: gymnaste
Returns:
ne retourne rien
"""
self.y = 23*cm
self.add_new_line(
X,
"Best ToF",
font_decoration="Bold",
)
has_score = False
data = []
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=0)
.order_by("-score")
.first()
)
if best_tof:
data.append(["ToF |:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
has_score = True
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=1)
.order_by("-score")
.first()
)
if best_tof:
data.append(["ToF R1:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
has_score = True
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=2)
.order_by("-score")
.first()
)
if best_tof:
data.append(["ToF R2:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
has_score = True
if has_score:
style = TableStyle(
[
('TEXTCOLOR', (-1,0), (-1,-1), '#AAAAAA'),
]
)
table = Table(data)
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
table.drawOn(self.document, X - 6, self.y - height - 5)
else:
self.add_new_line(
X,
"No chrono for this gymnast.",
)
self.y = 20*cm
self.add_new_line(
X,
"Best Scores",
font_decoration="Bold",
)
has_score = False
data = [["", "Exe.", "Diff.", "ToF", "HD", "Tot.", ""]]
best_point_routine_1 = Point.objects.filter(gymnast=gymnast).filter(routine_type=1).order_by('-total').first()
if best_point_routine_1:
data.append(
[
"R1: ",
best_point_routine_1.point_execution,
best_point_routine_1.point_difficulty,
best_point_routine_1.point_time_of_flight,
best_point_routine_1.point_horizontal_displacement,
best_point_routine_1.total,
best_point_routine_1.event.date_begin.strftime("%d-%m-%Y"),
]
)
has_score = True
best_point_routine_2 = Point.objects.filter(gymnast=gymnast).filter(routine_type=2).order_by('-total').first()
if best_point_routine_2:
data.append(
[
"R2 :",
best_point_routine_2.point_execution,
best_point_routine_2.point_difficulty,
best_point_routine_2.point_time_of_flight,
best_point_routine_2.point_horizontal_displacement,
best_point_routine_2.total,
best_point_routine_2.event.date_begin.strftime("%d-%m-%Y"),
]
)
has_score = True
else:
data.append(
[
"R2:",
"-",
"-",
"-",
"-",
"-",
]
)
if has_score:
style = TableStyle(
[
('ALIGN', (0,0), (-1,0), 'CENTER'),
('ALIGN', (1,1), (-1,-1), 'RIGHT'),
('TEXTCOLOR', (-1,0), (-1,-1), '#AAAAAA'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
# ('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data)
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
table.drawOn(self.document, X - 6, self.y - height - 5)
else:
self.add_new_line(
X,
"No scores for this gymnast.",
)
self.add_vspace(HUGE_LINE_HEIGHT)
def add_gymnast_active_routine(self, gymnast):
""" Ajoute les routines actives """
self.y = 23*cm
self.add_new_line(
15.9*cm,
"Routines",
font_decoration="Bold",
)
routine_1 = gymnast.has_routine.filter(routine_type=1).filter(date_begin__lte=date.today()).filter(Q(date_end__gte=date.today()) | Q(date_end__isnull=True)).first()
if routine_1:
data = []
for routine_skill in routine_1.routine.skill_links.all():
data.append([routine_skill.skill.notation, routine_skill.skill.difficulty])
data.append([None, routine_1.routine.difficulty])
style = TableStyle(
[
('ALIGN', (1,0), (1,-1), 'RIGHT'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2*cm, 1*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
table.drawOn(self.document, 13.5*cm, self.y - height - 5)
else:
self.add_new_line(
14*cm,
"No compulsary"
)
self.add_new_line(
14*cm,
"routine defined."
)
self.add_vspace(-DOUBLE_LINE_HEIGHT)
routine_2 = gymnast.has_routine.filter(routine_type=2).filter(date_begin__lte=date.today()).filter(Q(date_end__gte=date.today()) | Q(date_end__isnull=True)).first()
if routine_2:
data = []
for routine_skill in routine_2.routine.skill_links.all():
data.append([routine_skill.skill.notation, routine_skill.skill.difficulty])
data.append([None, routine_2.routine.difficulty])
style = TableStyle(
[
('ALIGN', (1,0), (1,-1), 'RIGHT'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2*cm, 1*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15.5*cm)
table.drawOn(self.document, 17*cm, self.y - height - 5)
else:
self.add_new_line(
17*cm,
"No volontary"
)
self.add_new_line(
17*cm,
"routine defined."
)
def add_gymnast_last_learned_skill(self, gymnast):
""" Ajoute les derniers skill appris par le gymnaste
Args:
gymnast <Gymnast> gymnaste
Returns:
Ne retourne rien
"""
self.y = 17*cm
self.add_new_line(
7.5*cm,
"New learned skills",
font_decoration="Bold",
)
self.add_vspace(-3)
# le double F ne fonctionne qu'en précisant le distinct, sinon ca dédouble les résultats.
# qui lui même ne fonctionne que sur un champ présent dans le `order_by` (que le premier champ ?)
#
learned_skills = (
LearnedSkill.objects.filter(gymnast=gymnast.id)
.annotate(skill_notation=F("skill__notation"))
.order_by("skill_notation", "-date").distinct('skill_notation')[:5]
)
if learned_skills:
for learned_skill in learned_skills:
self.add_new_line(
7.5*cm, learned_skill.skill.short_label + " " + str(LEARNING_STEP_CHOICES[learned_skill.learning_step][1]).lower() + " (" + learned_skill.skill.notation + "), " + learned_skill.date.strftime("%d-%m-%Y")
)
else:
self.add_new_line(
7.5*cm,
"No skill to learn this week.",
)
def add_gymnast_planned_skill(self, gymnast):
""" Ajoute les prochains skill (skill planifié) à apprendre
Args:
gymnast <Gymnast> gymnaste
Returns:
Ne retourne rien
"""
self.y = 17*cm
self.add_new_line(
X,
"Next skills to learn",
font_decoration="Bold",
)
self.add_vspace(-3)
# le double F ne fonctionne qu'en précisant le distinct, sinon ca dédouble les résultats.
# qui lui même ne fonctionne que sur un champ présent dans le `order_by` (que le premier champ ?)
#
print(gymnast)
# planned_skills = (
# Skill.objects.filter(plan__gymnast=gymnast.id)
# # .annotate(plan_date=F("plan__date"))
# .annotate(plan_date=F("plan__date"), learning_step=F("plan__learning_step"), plan_id=F("plan__id"))
# .order_by("id", "-plan__date").distinct('id')[:6]
# )
plan_list = (
Plan.objects.filter(gymnast=gymnast, educative__in=(Skill.objects.all()))
.filter(
Q(is_done=False)
| Q(date__gte=date.today())
)
.order_by('educative', '-date').distinct()[:6]
)
if plan_list:
for plan in plan_list:
print(plan)
# self.add_new_line(
# X, plan.educative.short_label + " " + str(LEARNING_STEP_CHOICES[plan.learning_step][1]).lower() + " (" + plan.educative.notation + ") for " + plan.date.strftime("%d-%m-%Y")
# )
skill = Skill.objects.get(pk=plan.educative)
self.add_new_line(
X, plan.educative.short_label + " " + str(LEARNING_STEP_CHOICES[plan.learning_step][1]).lower() + " (" + skill.notation + ") for " + plan.date.strftime("%d-%m-%Y")
)
else:
self.add_new_line(
X,
"No next skill to learn plannified.",
)
print()
print()
return plan_list
def add_gymnast_next_events(self, gymnast):
""" Ajoute les évènements futurs du gymnaste """
self.y = 13.5*cm
self.add_new_line(
X,
"Next event",
font_decoration="Bold",
)
self.add_vspace(-3)
today = pendulum.now().date()
next_event_list = Event.objects.filter(gymnasts=gymnast.id, date_begin__gte=today).order_by("date_begin")[:5]
data = []
for event in next_event_list:
data.append([event.date_begin.strftime("%d-%m-%Y"), "in " + str(event.number_of_week_from_today) + " week(s)", event.name])
if data:
style = TableStyle(
[
('ALIGN', (1,0), (1,-1), 'CENTER'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
# ('GRID', (0,0), (-1,-1), 0.25, colors.black),
# ('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2.3*cm, 2.2*cm, 8*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15.5*cm)
table.drawOn(self.document, X - 6, self.y - height - 5)
else:
self.add_new_line(
X,
"No futur event associated to this gymnast.",
)
def add_gymnast_week_notes(self, gymnast):
""" Ajoute les notes de la semaine du gymnaste passé en paramètre """
self.y = 8.8*cm
self.add_new_line(
X,
"Notes",
font_decoration="Bold",
)
self.add_vspace(-2*cm)
today = pendulum.today().date()
begin_of_the_week = today
if today.weekday() != 0:
begin_of_the_week -= timedelta(today.weekday())
notes = gymnast.remarks.filter(created_at__gte=begin_of_the_week).filter(status=1)
if notes:
html_text = ''
for note in notes:
html_text += '<br />' + note.to_markdown()
html_text = html_text[6:]
# print(html_text)
paragraph = Paragraph(html_text, self.style)
width, height = paragraph.wrap(18*cm, 10*cm)
paragraph.drawOn(self.document, X, self.y - (height / 1.5))
else:
self.add_vspace(1.8*cm)
self.add_new_line(
X,
"No note associated to this gymnast this week.",
)
def add_planned_skills_details(self, plan_list):
""" """
# self.y = 20*cm
self.add_new_line(
X,
"Points of attention",
font_decoration="Bold",
)
self.add_vspace(-0.5*cm)
for plan in plan_list:
if plan.informations:
print(plan)
planned_skill = Skill.objects.get(pk=plan.educative)
print(planned_skill)
# Titre du skill
html_text = "<u>" + planned_skill.short_label + " (" + planned_skill.notation + ") :</u>"
paragraph = Paragraph(html_text, self.style)
width, height = paragraph.wrap(18*cm, 10*cm)
paragraph.drawOn(self.document, INDENTED_X, self.y)
self.add_vspace(- height - 0.5*cm)
# Informations du skill pour le gymnaste
paragraph = Paragraph(plan.informations, self.style)
width, height = paragraph.wrap(18*cm, 10*cm)
paragraph.drawOn(self.document, INDENTED_X, self.y)
self.add_vspace(- height)
self.add_vspace(-0.4*cm)