Ultron/ultron/tools/pdf_generator.py

514 lines
16 KiB
Python

from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.lib import colors
from django.conf import settings
from datetime import date, datetime, timedelta
from django.db.models import Q, Max
import os
import locale
from PIL import Image
from ultron.people.models import Gymnast
from ultron.followup.models import (
Plan,
Point,
Chrono,
Accident,
MindState,
HeightWeight,
)
from ultron.objective.models import Skill
from statistics import mean
import os
import yaml
import re
# EXPENSES = 0
# RECETTES = 1
X = 35
Y = 841.89
INDENT = 5
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.__load_config()
def __load_config(self):
""" Charge le contenu du fichier SITE_CONFIG.YAML qui contient les données relatives à
l'ASBL.
"""
current_path = os.path.dirname(os.path.realpath(__file__))
self.club_infos = None
with open(os.path.join(current_path, "site_config.yaml"), "r") as stream:
try:
self.club_infos = yaml.load(stream, Loader=yaml.FullLoader)
except yaml.YAMLError as exc:
print(exc)
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_new_line(X, self.club_infos["SITE_TITLE"] + ' - ' + self.club_infos['CLUB_NAME'])
self.document.drawRightString(
RIGHT_X,
self.y,
self.club_infos["ADDRESS"]
+ " - "
+ self.club_infos["ZIP"]
+ " "
+ self.club_infos["CITY"],
)
self.add_new_line(
X,
"Head Coach : "
+ self.club_infos["HEAD_COACH"]
)
# N° de semaine
self.document.drawRightString(
RIGHT_X,
self.y,
self.club_infos["MOBILE_PHONE"]
)
# self.add_new_line(
# INDENTED_X,
# "Head Coach : "
# + self.club_infos["HEAD_COACH"]
# + " ("
# + self.club_infos["MOBILE_PHONE"]
# + ")"
# )
# if contract is not None:
# self.document.drawRightString(
# INDENTED_RIGHT_X, self.y, "N° de Référence : " + str(contract.reference)
# )
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)
self.add_gymnast_next_skills(gymnast)
# self.add_gymnast_active_routine(gymnast)
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.add_vspace()
self.document.setFillColorRGB(0.75, 0.75, 0.75)
self.add_new_line(
130,
str(gymnast.age)
)
self.document.setFillColorRGB(0, 0, 0)
self.add_new_line(
130,
str(gymnast.informations)
)
# self.add_vspace(HUGE_LINE_HEIGHT)
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
value_list
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])
res = self.analyse_score(last_mindstate.score, lasts_mindstate)
data.append(["Mind state", str(last_mindstate.score), res])
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])
res = self.analyse_score(last_height_weigth.height, lasts_height)
data.append(["Height", str(last_height_weigth.height), res])
res = self.analyse_score(last_height_weigth.weight, lasts_weight)
data.append(["Weight", str(last_height_weigth.weight), res])
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.5*cm, self.y - height - 5)
# last_accident = Accident.objects.filter(gymnast=gymnast).order_by("-date").first()
# print(last_accident)
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",
)
# self.add_vspace()
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=0)
.order_by("-score")
.first()
)
data = [
["ToF |:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"],
]
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=1)
.order_by("-score")
.first()
)
data.append(["ToF R1:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=2)
.order_by("-score")
.first()
)
data.append(["ToF R2:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
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)
# self.add_vspace(self.y - height - 5)
self.y = 20*cm
self.add_new_line(
X,
"Best Scores",
font_decoration="Bold",
)
data = [["", "Exe.", "Diff.", "ToF", "HD", "Tot.", ""]]
best_point_routine_1 = Point.objects.filter(gymnast=gymnast).filter(routine_type=1).order_by('-total').first()
print(best_point_routine_1)
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"),
]
)
best_point_routine_2 = Point.objects.filter(gymnast=gymnast).filter(routine_type=2).order_by('-total').first()
print(best_point_routine_2)
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"),
]
)
else:
data.append(
[
"R2:",
"-",
"-",
"-",
"-",
"-",
]
)
print(data)
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)
# routine_1 = gymnast.has_routine.prefetch_related("routine").filter(
# date_end__isnull=True
# )
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()
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()
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)
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)
def add_gymnast_next_skills(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)
planified_skills = (
Skill.objects.filter(plan__gymnast=gymnast.id)
.filter(
Q(plan__is_done=False)
| Q(plan__date__gte=date.today())
)
.order_by("-plan__date")[:6]
)
# Ne permet pas de récupérer que les skill, or je voudrais bien.
# planified_skills = (
# Plan.objects.filter(gymnast=gymnast.id)
# .filter(
# Q(is_done=False)
# | Q(date__gte=date.today())
# )
# .order_by("-date")[:6]
# )
for planified_skill in planified_skills:
self.add_new_line(
X, planified_skill.skill.short_label + " (" + planified_skill.skill.notation + ") for (todo: compute deadline)"
)