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 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 (Accident, Chrono, HeightWeight, MindState, Plan, Point) from ultron.objective.models import Skill from ultron.people.models import Gymnast from ultron.planning.models import Event from .models import get_number_of_week_from_begin_of_season 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 = 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.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 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) self.document.drawRightString(RIGHT_X, self.y, "Week " + str(get_number_of_week_from_begin_of_season())) 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_next_events(gymnast) self.add_gymnast_week_notes(gymnast) def add_gymnast_personnal_information(self, gymnast): """ Ajoute les informations personnelles du gymnast. Args: 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 float valeur value_list array 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 : 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.", ) # 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 : gymnaste Returns: ne retourne rien """ self.y = 23*cm self.add_new_line( X, "Best ToF", font_decoration="Bold", ) has_score = False best_tof = ( Chrono.objects.filter(gymnast=gymnast) .filter(chrono_type=0) .order_by("-score") .first() ) if best_tof: data = [ ["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.", ) # 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() 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.5*cm, "No compulsary routine defined." ) 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( 14.5*cm, "No volontary routine defined." ) def add_gymnast_next_skills(self, gymnast): """ Ajoute les prochains skill (skill planifié) à apprendre Args: 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] # ) if planified_skills: for planified_skill in planified_skills: self.add_new_line( X, planified_skill.skill.short_label + " (" + planified_skill.skill.notation + ") for (todo: compute deadline)" ) else: self.add_new_line( X, "No next skill to learn.", ) 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 = 10.3*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) if notes: html_text = '' for note in notes: html_text += '
' + note.to_markdown() html_text = html_text[6:] # print(html_text) paragraph = Paragraph(html_text, self.style, bulletText='*') width, height = paragraph.wrap(18*cm, 18*cm) paragraph.drawOn(self.document, X, self.y) else: self.add_vspace(1.8*cm) self.add_new_line( X, "No note associated to this gymnast this week.", )