diff --git a/common/settings.py b/common/settings.py index 7d69991..9444467 100644 --- a/common/settings.py +++ b/common/settings.py @@ -151,4 +151,6 @@ INSTRUCTOR_IMPORT_MAPPING = { 'MAIL_FORMATEUR': 'email', } +CHARGE_SHEET_TITLE = "Feuille de charge pour l'année scolaire 2017-2018" + from .local_settings import * diff --git a/requirements.txt b/requirements.txt index 4c46249..c43a59c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Django==1.11.2 tabimport>=0.4.0 openpyxl==2.2.6 xlrd +reportlab diff --git a/stages/admin.py b/stages/admin.py index bd0bdcc..16a8aa1 100644 --- a/stages/admin.py +++ b/stages/admin.py @@ -1,11 +1,40 @@ +import os +import tempfile +import zipfile + from django import forms from django.contrib import admin from django.db import models +from django.http import HttpResponse from stages.models import ( Teacher, Student, Section, Level, Klass, Referent, Corporation, CorpContact, Domain, Period, Availability, Training, Course, ) +from stages.pdf import ChargeSheetPDF + + +def print_charge_sheet(modeladmin, request, queryset): + """ + Génère un pdf pour chaque enseignant, écrit le fichier créé + dans une archive et renvoie une archive de pdf + """ + filename = 'archive_FeuillesDeCharges.zip' + path = os.path.join(tempfile.gettempdir(), filename) + + with zipfile.ZipFile(path, mode='w', compression=zipfile.ZIP_DEFLATED) as filezip: + for teacher in queryset: + activities = teacher.calc_activity() + pdf = ChargeSheetPDF(teacher) + pdf.produce(activities) + filezip.write(pdf.filename) + + with open(filezip.filename, mode='rb') as fh: + response = HttpResponse(fh.read(), content_type='application/zip') + response['Content-Disposition'] = 'attachment; filename="{0}"'.format(filename) + return response + +print_charge_sheet.short_description = "Imprimer les feuilles de charge" class ArchivedListFilter(admin.BooleanFieldListFilter): @@ -35,6 +64,7 @@ class KlassAdmin(admin.ModelAdmin): class TeacherAdmin(admin.ModelAdmin): list_display = ('__str__', 'abrev', 'email') + actions = [print_charge_sheet] class StudentAdmin(admin.ModelAdmin): diff --git a/stages/models.py b/stages/models.py index f5888e4..b816752 100644 --- a/stages/models.py +++ b/stages/models.py @@ -78,6 +78,29 @@ class Teacher(models.Model): def __str__(self): return '{0} {1}'.format(self.last_name, self.first_name) + def calc_activity(self): + """ + Return a dictionary of calculations relative to teacher courses. + """ + mandats = self.course_set.filter(subject__startswith='#') + ens = self.course_set.exclude(subject__startswith='#') + tot_mandats = mandats.aggregate(models.Sum('period'))['period__sum'] or 0 + tot_ens = ens.aggregate(models.Sum('period'))['period__sum'] or 0 + tot_formation = int(round((tot_mandats + tot_ens) / 1900 * 250)) + tot_trav = self.previous_report + tot_mandats + tot_ens + tot_formation + tot_paye = tot_trav + if self.rate == 100 and tot_paye != 100: + tot_paye = 2150 + return { + 'mandats': mandats, + 'tot_mandats': tot_mandats, + 'tot_ens': tot_ens, + 'tot_formation': tot_formation, + 'tot_trav': tot_trav, + 'tot_paye': tot_paye, + 'report': tot_trav - tot_paye, + } + class Student(models.Model): ext_id = models.IntegerField(null=True, unique=True, verbose_name='ID externe') diff --git a/stages/pdf.py b/stages/pdf.py new file mode 100644 index 0000000..5370f40 --- /dev/null +++ b/stages/pdf.py @@ -0,0 +1,83 @@ +import os +import tempfile +from datetime import date + +from django.conf import settings +from django.contrib.staticfiles.finders import find + +from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, + PageBreak, Table, TableStyle, Image) +from reportlab.lib.pagesizes import A4, landscape +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT +from reportlab.lib import colors +from reportlab.lib.styles import ParagraphStyle as PS + +style_8_c = PS(name='CORPS', fontName='Helvetica', fontSize=6, alignment = TA_CENTER) +style_normal = PS(name='CORPS', fontName='Helvetica', fontSize=8, alignment = TA_LEFT) +style_mandat = PS(name='CORPS', fontName='Helvetica', fontSize=8, alignment = TA_LEFT, leftIndent=30) +style_bold = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=10, alignment = TA_LEFT) +style_title = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=12, alignment = TA_LEFT, spaceBefore=2*cm) +style_adress = PS(name='CORPS', fontName='Helvetica', fontSize=10, alignment = TA_LEFT, leftIndent=280) +style_normal_right = PS(name='CORPS', fontName='Helvetica', fontSize=8, alignment = TA_RIGHT) + + +class ChargeSheetPDF(SimpleDocTemplate): + """ + Génération des feuilles de charges en pdf. + """ + + def __init__(self, teacher): + self.teacher = teacher + filename = '{0}_{1}.pdf'.format(teacher.last_name, teacher.first_name) + path = os.path.join(tempfile.gettempdir(), filename) + super().__init__(path, pagesize=A4, topMargin=0*cm, leftMargin=2*cm) + + def produce(self, activities): + self.story = [] + self.story.append(Image(find('img/header.gif'), width=520, height=75)) + self.story.append(Spacer(0, 2*cm)) + destinataire = '{0}
{1}'.format(self.teacher.civility, str(self.teacher)) + self.story.append(Paragraph(destinataire, style_adress)) + self.story.append(Spacer(0, 2*cm)) + + data = [[settings.CHARGE_SHEET_TITLE]] + + data.append(["Report de l'année précédente", + '{0:3d} pér.'.format(self.teacher.previous_report) ]) + data.append(['Mandats', + '{0:3d} pér.'.format(activities['tot_mandats'])]) + + for act in activities['mandats']: + data.append([' * {0} ({1} pér.)'.format(act.subject, act.period)]) + + data.append(['Enseignement (coef.2)', + '{0:3d} pér.'.format(activities['tot_ens'])]) + data.append(['Formation continue et autres tâches', + '{0:3d} pér.'.format(activities['tot_formation'])]) + data.append(['Total des heures travaillées', + '{0:3d} pér.'.format(activities['tot_trav']), + '{0:4.1f} %'.format(activities['tot_trav']/21.50)]) + data.append(['Total des heures payées', + '{0:3d} pér.'.format(activities['tot_paye']), + '{0:4.1f} %'.format(activities['tot_paye']/21.50)]) + data.append(["Report à l'année prochaine", + '{0:3d} pér.'.format(activities['report'])]) + + t = Table(data, colWidths=[12*cm, 2*cm, 2*cm] ) + t.setStyle(TableStyle([('ALIGN',(1,0),(-1,-1),'RIGHT'), + ('FONT', (0,0),(-1,0), 'Helvetica-Bold'), + ('LINEBELOW', (0,0),(-1,0), 0.5, colors.black), + ('LINEABOVE', (0,-3) ,(-1,-1), 0.5, colors.black), + ('FONT', (0,-2),(-1,-2), 'Helvetica-Bold'), + ])) + t.hAlign = TA_CENTER + self.story.append(t) + + self.story.append(Spacer(0, 2*cm)) + d = 'La Chaux-de-Fonds, le {0}'.format(date.today().strftime('%d.%m.%y')) + self.story.append(Paragraph(d, style_normal)) + self.story.append(Spacer(0, 0.5*cm)) + self.story.append(Paragraph('la direction', style_normal)) + self.story.append(PageBreak()) + self.build(self.story) diff --git a/stages/static/img/header.gif b/stages/static/img/header.gif new file mode 100644 index 0000000..a02c592 Binary files /dev/null and b/stages/static/img/header.gif differ diff --git a/stages/tests.py b/stages/tests.py index add51c6..8a93174 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -149,6 +149,28 @@ class PeriodTest(TestCase): self.assertEqual(per.weeks, 2) +class TeacherTests(TestCase): + def setUp(self): + User.objects.create_superuser('me', 'me@example.org', 'mepassword') + + def test_export_charge_sheet(self): + Teacher.objects.create( + first_name='Laurie', last_name='Bernasconi', birth_date='1974-08-08' + ) + change_url = reverse('admin:stages_teacher_changelist') + self.client.login(username='me', password='mepassword') + response = self.client.post(change_url, { + 'action': 'print_charge_sheet', + '_selected_action': Teacher.objects.values_list('pk', flat=True) + }, follow=True) + self.assertEqual( + response['Content-Disposition'], + 'attachment; filename="archive_FeuillesDeCharges.zip"' + ) + self.assertEqual(response['Content-Type'], 'application/zip') + self.assertGreater(len(response.content), 200) + + class ImportTests(TestCase): def setUp(self): User.objects.create_user('me', 'me@example.org', 'mepassword')