diff --git a/candidats/admin.py b/candidats/admin.py index e1684c6..a291677 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -5,7 +5,7 @@ from django.db.models import BooleanField from django.urls import reverse from django.utils.html import format_html -from stages.exports import OpenXMLExport +from stages.views.export import OpenXMLExport from .forms import CandidateForm from .models import ( Candidate, Interview, GENDER_CHOICES, DIPLOMA_CHOICES, DIPLOMA_STATUS_CHOICES, diff --git a/candidats/tests.py b/candidats/tests.py index ed2a677..56b49ed 100644 --- a/candidats/tests.py +++ b/candidats/tests.py @@ -6,7 +6,7 @@ from django.core import mail from django.test import TestCase from django.urls import reverse -from stages.exports import openxml_contenttype +from stages.views.export import openxml_contenttype from stages.models import Section, Teacher from .models import Candidate, Interview diff --git a/candidats/views.py b/candidats/views.py index 46fabb5..15bb8fd 100644 --- a/candidats/views.py +++ b/candidats/views.py @@ -8,7 +8,7 @@ from django.template import loader from django.urls import reverse, reverse_lazy from django.utils import timezone -from stages.base_views import EmailConfirmationBaseView +from stages.views.base import EmailConfirmationBaseView from candidats.models import Candidate, Interview from .pdf import InscriptionSummaryPDF diff --git a/common/urls.py b/common/urls.py index 32849c0..7dd62f4 100644 --- a/common/urls.py +++ b/common/urls.py @@ -18,7 +18,7 @@ urlpatterns = [ path('import_hp_contacts/', views.HPContactsImportView.as_view(), name='import-hp-contacts'), path('attribution/', views.AttributionView.as_view(), name='attribution'), - re_path(r'^stages/export/(?Pall)?/?$', views.stages_export, name='stages_export'), + re_path(r'^stages/export/(?Pall)?/?$', views.export.stages_export, name='stages_export'), path('institutions/', views.CorporationListView.as_view(), name='corporations'), path('institutions//', views.CorporationView.as_view(), name='corporation'), @@ -42,14 +42,14 @@ urlpatterns = [ name='print-expert-compens-ede'), path('student_ede//examination/mentor/', views.print_mentor_ede_compensation_form, name='print-mentor-compens-ede'), - path('student_ede/export_qualif_ede/', views.export_qualification_ede, + path('student_ede/export_qualif_ede/', views.export.export_qualification_ede, name='export-qualif-ede'), - path('imputations/export/', views.imputations_export, name='imputations_export'), - path('export_sap/', views.export_sap, name='export_sap'), + path('imputations/export/', views.export.imputations_export, name='imputations_export'), + path('export_sap/', views.export.export_sap, name='export_sap'), path('print/update_form/', views.print_update_form, name='print_update_form'), - path('general_export/', views.general_export, name='general-export'), - path('ortra_export/', views.ortra_export, name='ortra-export'), + path('general_export/', views.export.general_export, name='general-export'), + path('ortra_export/', views.export.ortra_export, name='ortra-export'), # AJAX/JSON urls path('section//periods/', views.section_periods, name='section_periods'), diff --git a/stages/exports.py b/stages/exports.py deleted file mode 100644 index 1114fc1..0000000 --- a/stages/exports.py +++ /dev/null @@ -1,41 +0,0 @@ -from datetime import date - -from openpyxl import Workbook -from openpyxl.styles import Font -from openpyxl.utils import get_column_letter -from openpyxl.writer.excel import save_virtual_workbook - -from django.http import HttpResponse - -openxml_contenttype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - - -class OpenXMLExport: - def __init__(self, sheet_title): - self.wb = Workbook() - self.ws = self.wb.active - self.ws.title = sheet_title - self.bold = Font(bold=True) - self.row_idx = 1 - - def write_line(self, values, bold=False, col_widths=()): - for col_idx, value in enumerate(values, start=1): - cell = self.ws.cell(row=self.row_idx, column=col_idx) - try: - cell.value = value - except KeyError: - # Ugly workaround for https://bugs.python.org/issue28969 - from openpyxl.utils.datetime import to_excel - to_excel.cache_clear() - cell.value = value - if bold: - cell.font = self.bold - if col_widths: - self.ws.column_dimensions[get_column_letter(col_idx)].width = col_widths[col_idx - 1] - self.row_idx += 1 - - def get_http_response(self, filename_base): - response = HttpResponse(save_virtual_workbook(self.wb), content_type=openxml_contenttype) - response['Content-Disposition'] = 'attachment; filename=%s_%s.xlsx' % ( - filename_base, date.strftime(date.today(), '%Y-%m-%d')) - return response diff --git a/stages/utils.py b/stages/utils.py index 01fb4b2..a3c3797 100644 --- a/stages/utils.py +++ b/stages/utils.py @@ -1,3 +1,6 @@ +from datetime import date + + def school_year(date, as_tuple=False): """ Return the school year of 'date'. Example: @@ -14,6 +17,15 @@ def school_year(date, as_tuple=False): return "%d — %d" % (start_year, start_year + 1) +def school_year_start(): + """ Return first official day of current school year """ + current_year = date.today().year + if date(current_year, 8, 1) > date.today(): + return date(current_year-1, 8, 1) + else: + return date(current_year, 8, 1) + + def is_int(s): try: int(s) diff --git a/stages/views.py b/stages/views/__init__.py similarity index 65% rename from stages/views.py rename to stages/views/__init__.py index 58516a2..4f7989f 100644 --- a/stages/views.py +++ b/stages/views/__init__.py @@ -27,24 +27,15 @@ from django.utils.translation import ugettext as _ from django.utils.text import slugify from django.views.generic import DetailView, FormView, TemplateView, ListView -from .base_views import EmailConfirmationBaseView -from .exports import OpenXMLExport -from .forms import EmailBaseForm, PeriodForm, StudentImportForm, UploadHPFileForm, UploadReportForm -from .models import ( +from .base import EmailConfirmationBaseView +from .export import OpenXMLExport +from ..forms import EmailBaseForm, PeriodForm, StudentImportForm, UploadHPFileForm, UploadReportForm +from ..models import ( Klass, Section, Option, Student, Teacher, Corporation, CorpContact, Course, Period, Training, Availability ) -from .pdf import ExpertEdeLetterPdf, UpdateDataFormPDF, MentorCompensationPdfForm, KlassListPDF -from .utils import is_int - - -def school_year_start(): - """ Return first official day of current school year """ - current_year = date.today().year - if date(current_year, 8, 1) > date.today(): - return date(current_year-1, 8, 1) - else: - return date(current_year, 8, 1) +from ..pdf import ExpertEdeLetterPdf, UpdateDataFormPDF, MentorCompensationPdfForm, KlassListPDF +from ..utils import is_int, school_year_start class CorporationListView(ListView): @@ -749,242 +740,6 @@ class StudentConvocationExaminationView(EmailConfirmationView): student.save() -EXPORT_FIELDS = [ - # Student fields - ('ID externe', 'student__ext_id'), - ('Prénom', 'student__first_name'), ('Nom', 'student__last_name'), - ('Titre', 'student__gender'), - ('Classe', 'student__klass__name'), - ('Filière', 'student__klass__section__name'), - ('Rue élève', 'student__street'), - ('NPA_élève', 'student__pcode'), - ('Localité élève', 'student__city'), - ('Tél élève', 'student__tel'), - ('Email élève', 'student__email'), - ('Date de naissance', 'student__birth_date'), - ('No AVS', 'student__avs'), - # Stage fields - ('Nom du stage', 'availability__period__title'), - ('Début', 'availability__period__start_date'), ('Fin', 'availability__period__end_date'), - ('Remarques stage', 'comment'), - ('Prénom référent', 'referent__first_name'), ('Nom référent', 'referent__last_name'), - ('Courriel référent', 'referent__email'), - ('Institution', 'availability__corporation__name'), - ('ID externe Inst', 'availability__corporation__ext_id'), - ('Rue Inst', 'availability__corporation__street'), - ('NPA Inst', 'availability__corporation__pcode'), - ('Ville Inst', 'availability__corporation__city'), - ('Tél Inst', 'availability__corporation__tel'), - ('Domaine', 'availability__domain__name'), - ('Remarques Inst', 'availability__comment'), - ('Civilité contact', 'availability__contact__civility'), - ('Prénom contact', 'availability__contact__first_name'), - ('Nom contact', 'availability__contact__last_name'), - ('ID externe contact', 'availability__contact__ext_id'), - ('Tél contact', 'availability__contact__tel'), - ('Courriel contact', 'availability__contact__email'), - ('Courriel contact - copie', None), -] - - -NON_ATTR_EXPORT_FIELDS = [ - ('Filière', 'period__section__name'), - ('Nom du stage', 'period__title'), - ('Début', 'period__start_date'), ('Fin', 'period__end_date'), - ('Institution', 'corporation__name'), - ('Rue Inst', 'corporation__street'), - ('NPA Inst', 'corporation__pcode'), - ('Ville Inst', 'corporation__city'), - ('Tél Inst', 'corporation__tel'), - ('Domaine', 'domain__name'), - ('Remarques Inst', 'comment'), - ('Civilité contact', 'contact__civility'), - ('Prénom contact', 'contact__first_name'), - ('Nom contact', 'contact__last_name'), - ('Tél contact', 'contact__tel'), - ('Courriel contact', 'contact__email'), - ('Courriel contact - copie', None), -] - - -def stages_export(request, scope=None): - period_filter = request.GET.get('period') - non_attributed = bool(int(request.GET.get('non_attr', 0))) - - export_fields = OrderedDict(EXPORT_FIELDS) - contact_test_field = 'availability__contact__last_name' - corp_name_field = 'availability__corporation__name' - - if period_filter: - if non_attributed: - # Export non attributed availabilities for a specific period - query = Availability.objects.filter(period_id=period_filter, training__isnull=True) - export_fields = OrderedDict(NON_ATTR_EXPORT_FIELDS) - contact_test_field = 'contact__last_name' - corp_name_field = 'corporation__name' - else: - # Export trainings for a specific period - query = Training.objects.filter(availability__period_id=period_filter) - else: - if scope and scope == 'all': - # Export all trainings in the database - query = Training.objects.all() - else: - query = Training.objects.filter(availability__period__end_date__gt=school_year_start()) - - # Prepare "default" contacts (when not defined on training) - section_names = Section.objects.all().values_list('name', flat=True) - default_contacts = dict( - (c, {s: '' for s in section_names}) - for c in Corporation.objects.all().values_list('name', flat=True) - ) - always_ccs = dict( - (c, {s: [] for s in section_names}) - for c in Corporation.objects.all().values_list('name', flat=True) - ) - for contact in CorpContact.objects.filter(corporation__isnull=False - ).select_related('corporation' - ).prefetch_related('sections').order_by('corporation'): - for section in contact.sections.all(): - if not default_contacts[contact.corporation.name][section.name] or contact.is_main is True: - default_contacts[contact.corporation.name][section.name] = contact - if contact.always_cc: - always_ccs[contact.corporation.name][section.name].append(contact) - if contact.is_main: - for sname in section_names: - if not default_contacts[contact.corporation.name][sname]: - default_contacts[contact.corporation.name][sname] = contact - - export = OpenXMLExport('Stages') - export.write_line(export_fields.keys(), bold=True) # Headers - # Data - query_keys = [f for f in export_fields.values() if f is not None] - for line in query.values(*query_keys): - values = [] - for field in query_keys: - value = line[field] - if 'gender' in field: - value = {'F': 'Madame', 'M': 'Monsieur', '': ''}[value] - values.append(value) - if line[contact_test_field] is None: - # Use default contact - contact = default_contacts.get(line[corp_name_field], {}).get(line[export_fields['Filière']]) - if contact: - values = values[:-6] + [ - contact.civility, contact.first_name, contact.last_name, contact.ext_id, - contact.tel, contact.email - ] - if always_ccs[line[corp_name_field]].get(line[export_fields['Filière']]): - values.append("; ".join( - [c.email for c in always_ccs[line[corp_name_field]].get(line[export_fields['Filière']])] - )) - export.write_line(values) - - return export.get_http_response('stages_export') - - -def _ratio_Ede_Ase_Assc(): - # Spliting for unattribued periods - tot_edeps = Course.objects.filter(imputation='EDEps').aggregate(Sum('period'))['period__sum'] or 0 - tot_edepe = Course.objects.filter(imputation='EDEpe').aggregate(Sum('period'))['period__sum'] or 0 - edepe_ratio = 1 if tot_edepe + tot_edeps == 0 else tot_edepe / (tot_edepe + tot_edeps) - - tot_asefe = Course.objects.filter(imputation='ASEFE').aggregate(Sum('period'))['period__sum'] or 0 - tot_mpts = Course.objects.filter(imputation='MPTS').aggregate(Sum('period'))['period__sum'] or 0 - asefe_ratio = 1 if tot_asefe + tot_mpts == 0 else tot_asefe / (tot_asefe + tot_mpts) - - tot_asscfe = Course.objects.filter(imputation='ASSCFE').aggregate(Sum('period'))['period__sum'] or 0 - tot_mps = Course.objects.filter(imputation='MPS').aggregate(Sum('period'))['period__sum'] or 0 - asscfe_ratio = 1 if tot_asscfe + tot_mps == 0 else tot_asscfe / (tot_asscfe + tot_mps) - - return {'edepe':edepe_ratio, 'asefe':asefe_ratio, 'asscfe': asscfe_ratio} - - -def imputations_export(request): - IMPUTATIONS_EXPORT_FIELDS = [ - 'Nom', 'Prénom', 'Report passé', 'Ens', 'Discipline', - 'Accomp.', 'Discipline', 'Total payé', 'Indice', 'Taux', 'Report futur', - 'ASA', 'ASSC', 'ASE', 'MPTS', 'MPS', 'EDEpe', 'EDEps', 'EDS', 'CAS_FPP' - ] - - ratios = _ratio_Ede_Ase_Assc() - - export = OpenXMLExport('Imputations') - export.write_line(IMPUTATIONS_EXPORT_FIELDS, bold=True) # Headers - - for teacher in Teacher.objects.filter(archived=False): - activities, imputations = teacher.calc_imputations(ratios) - values = [ - teacher.last_name, teacher.first_name, teacher.previous_report, - activities['tot_ens'], 'Ens. prof.', activities['tot_mandats'] + activities['tot_formation'], - 'Accompagnement', activities['tot_paye'], 'Charge globale', - '{0:.2f}'.format(activities['tot_paye']/settings.GLOBAL_CHARGE_PERCENT), - teacher.next_report, - ] - values.extend(imputations.values()) - export.write_line(values) - - return export.get_http_response('Imputations_export') - - -def export_sap(request): - EXPORT_SAP_HEADERS = [ - 'PERNR', 'PERNOM', 'DEGDA', 'ENDDA', 'ZNOM', 'ZUND', - 'ZACT', 'ZBRA', 'ZOTP', 'ZCCO', 'ZORD', 'ZTAUX', - ] - MAPPING_OTP = { - 'ASAFE': 'CIFO01.03.02.03.01.02 - ASA EE', - 'ASEFE': 'CIFO01.03.02.04.01.02 - CFC ASE EE', - 'ASSCFE': 'CIFO01.03.02.04.02.02 - CFC ASSC EE', - 'EDEpe': 'CIFO01.03.02.07.01.01 - EDE prat. prof. PT', - 'EDEps': 'CIFO01.03.02.07.02.01 - EDE stages PT', - 'EDS': 'CIFO01.03.02.07.03.02 - EDS EE', - 'CAS_FPP': 'CIFO01.03.02.01.03 - Mandats divers (CAS FPP)', - 'MPTS' : 'CIFO01.04.03.06.02.01 - MPTS ASE', - 'MPS': 'CIFO01.04.03.06.03.01 - MPS Santé', - } - - ratios = _ratio_Ede_Ase_Assc() - - export = OpenXMLExport('Imputations') - export.write_line(EXPORT_SAP_HEADERS, bold=True) # Headers - start_date = '20.08.2018' - end_date = '19.08.2019' - indice = 'charge globale' - type_act = 'Ens. prof.' - branche = 'Ens. prof.' - centre_cout = '' - stat = '' - - for teacher in Teacher.objects.filter(archived=False): - activities, imputations = teacher.calc_imputations(ratios) - for key in imputations: - if imputations[key] > 0: - values = [ - teacher.ext_id, teacher.full_name, start_date, end_date, imputations[key], indice, type_act, - branche, MAPPING_OTP[key], centre_cout, stat, - round(imputations[key] / settings.GLOBAL_CHARGE_PERCENT, 2), - ] - export.write_line(values) - - # Previous report - values = [ - teacher.ext_id, teacher.full_name, start_date, end_date, teacher.previous_report, indice, type_act, - branche, 'Report précédent', centre_cout, stat, - round(teacher.previous_report / settings.GLOBAL_CHARGE_PERCENT, 2), - ] - export.write_line(values) - - # Next report - values = [ - teacher.ext_id, teacher.full_name, start_date, end_date, teacher.next_report, indice, type_act, - branche, 'Report suivant', centre_cout, stat, - round(teacher.next_report / settings.GLOBAL_CHARGE_PERCENT, 2), - ] - export.write_line(values) - return export.get_http_response('Export_SAP') - - def print_update_form(request): """ PDF form to update personal data @@ -1057,174 +812,3 @@ def print_klass_list(request): response = HttpResponse(fh.read(), content_type='application/zip') response['Content-Disposition'] = 'attachment; filename="{0}"'.format(filename) return response - - -GENERAL_EXPORT_FIELDS = [ - ('Num_Ele', 'ext_id'), - ('Nom_Ele', 'last_name'), - ('Prenom_Ele', 'first_name'), - ('Genre_Ele', 'gender'), - ('Rue_Ele', 'street'), - ('NPA_Ele', 'pcode'), - ('Ville_Ele', 'city'), - ('DateNaissance_Ele', 'birth_date'), - ('NOAVS_Ele', 'avs'), - ('Canton_Ele', 'district'), - ('Email_Ele', 'email'), - ('Mobile_Ele', 'mobile'), - ('DispenseCG_Ele', 'dispense_ecg'), - ('DispenseEPS_Ele', 'dispense_eps'), - ('SoutienDYS_Ele', 'soutien_dys'), - - ('Classe_Ele', 'klass__name'), - ('Filiere_Ele', 'klass__section__name'), - ('MaitreDeClasseNom_Ele', 'klass__teacher__last_name'), - ('MaitreDeClassePrenom_Ele', 'klass__teacher__first_name'), - ('OptionASE_Ele', 'option_ase__name'), - - ('Num_Emp', 'corporation__ext_id'), - ('Nom_Emp', 'corporation__name'), - ('Rue_Emp', 'corporation__street'), - ('NPA_Emp', 'corporation__pcode'), - ('Ville_Emp', 'corporation__city'), - ('Canton_Emp', 'corporation__district'), - ('Secteur_Emp', 'corporation__sector'), - ('Type_EMP', 'corporation__typ'), - ('Tel_Emp', 'corporation__tel'), - - ('Num_Form', 'instructor__ext_id'), - ('Titre_Form', 'instructor__civility'), - ('Prenom_Form', 'instructor__first_name'), - ('Nom_Form', 'instructor__last_name'), - ('Tel_Form', 'instructor__tel'), - ('Email_Form', 'instructor__email'), - ('EmailCopie_Form', None), -] - - -def general_export(request): - """ - Export all current students data - """ - export_fields = OrderedDict(GENERAL_EXPORT_FIELDS) - export = OpenXMLExport('Exportation') - export.write_line(export_fields.keys(), bold=True) # Headers - # Data - query_keys = [f for f in export_fields.values() if f is not None] - query = Student.objects.filter(archived=False).order_by('klass__name', 'last_name', 'first_name') - for line in query.values(*query_keys): - values = [] - for field in query_keys: - if field == 'gender': - values.append(('Madame', 'Monsieur')[line[field] == 'M']) - elif field in ('dispense_ecg', 'dispense_eps', 'soutien_dys'): - values.append('Oui' if line[field] is True else '') - else: - values.append(line[field]) - export.write_line(values) - - return export.get_http_response('general_export') - - -ORTRA_EXPORT_FIELDS = [ - ('Num_Ele', 'ext_id'), - ('Nom_Ele', 'last_name'), - ('Prenom_Ele', 'first_name'), - ('Genre_Ele', 'gender'), - ('Rue_Ele', 'street'), - ('NPA_Ele', 'pcode'), - ('Ville_Ele', 'city'), - ('DateNaissance_Ele', 'birth_date'), - ('Email_Ele', 'email'), - ('Mobile_Ele', 'mobile'), - - ('Classe_Ele', 'klass__name'), - ('Filiere_Ele', 'klass__section__name'), - ('MaitreDeClasseNom_Ele', 'klass__teacher__last_name'), - ('MaitreDeClassePrenom_Ele', 'klass__teacher__first_name'), - ('OptionASE_Ele', 'option_ase__name'), - - ('Num_Emp', 'corporation__ext_id'), - ('Nom_Emp', 'corporation__name'), - ('Rue_Emp', 'corporation__street'), - ('NPA_Emp', 'corporation__pcode'), - ('Ville_Emp', 'corporation__city'), - ('Tel_Emp', 'corporation__tel'), - - ('Titre_Form', 'instructor__civility'), - ('Prenom_Form', 'instructor__first_name'), - ('Nom_Form', 'instructor__last_name'), - ('Tel_Form', 'instructor__tel'), - ('Email_Form', 'instructor__email'), -] - - -def ortra_export(request): - """ - Export students data from sections ASAFE, ASEFE and ASSCFE - """ - export_fields = OrderedDict(ORTRA_EXPORT_FIELDS) - export = OpenXMLExport('Exportation') - export.write_line(export_fields.keys(), bold=True) # Headers - # Data - query_keys = [f for f in export_fields.values() if f is not None] - query = Student.objects.filter(Q(klass__name__contains='ASAFE') | - Q(klass__name__contains='ASEFE') | - Q(klass__name__contains='ASSCFE'), - archived=False).order_by('klass__name', - 'last_name', - 'first_name') - - for line in query.values(*query_keys): - values = [] - for field in query_keys: - if field == 'gender': - values.append(('Madame', 'Monsieur')[line[field] == 'M']) - else: - values.append(line[field]) - export.write_line(values) - - return export.get_http_response('ortra_export') - - -def export_qualification_ede(request): - headers = [ - 'Classe', 'Etudiant-e', - 'Référent pratique', 'Résumé TD', 'Ens. référent', 'dernier RDV', - 'Mentor', - 'Session', - 'Titre TD', - 'Exp_int.', - 'Expert ext. Civilité', 'Expert ext. Nom', 'Expert ext. Adresse', 'Expert ext. Localité', - 'Date', 'Salle', 'Note', - ] - - export = OpenXMLExport('Expor_Qualif_EDE') - export.write_line(headers, bold=True) - - # Data - for student in Student.objects.filter(klass__name__startswith='3EDE', archived=False - ).select_related('klass', 'referent', 'training_referent', 'mentor', 'expert', 'internal_expert', - ).order_by('klass__name', 'last_name'): - values = [ - student.klass.name, - student.full_name, - student.training_referent.full_name if student.training_referent else '', - student.subject, - student.referent.full_name if student.referent else '', - student.last_appointment, - student.mentor.full_name if student.mentor else '', - str(student.session), - student.title, - student.internal_expert.full_name if student.internal_expert else '', - student.expert.civility if student.expert else '', - student.expert.full_name if student.expert else '', - student.expert.street if student.expert else '', - student.expert.pcode_city if student.expert else '', - student.date_exam, - student.room, - student.mark, - ] - export.write_line(values) - - return export.get_http_response('Export_qualif_EDE') diff --git a/stages/base_views.py b/stages/views/base.py similarity index 100% rename from stages/base_views.py rename to stages/views/base.py diff --git a/stages/views/export.py b/stages/views/export.py new file mode 100644 index 0000000..780107c --- /dev/null +++ b/stages/views/export.py @@ -0,0 +1,457 @@ +from collections import OrderedDict +from datetime import date + +from django.conf import settings +from django.db.models import Q, Sum +from django.http import HttpResponse + +from openpyxl import Workbook +from openpyxl.styles import Font +from openpyxl.utils import get_column_letter +from openpyxl.writer.excel import save_virtual_workbook + +from ..models import ( + Availability, CorpContact, Corporation, Course, Section, Student, Teacher, + Training, +) +from ..utils import school_year_start + +openxml_contenttype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + + +class OpenXMLExport: + def __init__(self, sheet_title): + self.wb = Workbook() + self.ws = self.wb.active + self.ws.title = sheet_title + self.bold = Font(bold=True) + self.row_idx = 1 + + def write_line(self, values, bold=False, col_widths=()): + for col_idx, value in enumerate(values, start=1): + cell = self.ws.cell(row=self.row_idx, column=col_idx) + try: + cell.value = value + except KeyError: + # Ugly workaround for https://bugs.python.org/issue28969 + from openpyxl.utils.datetime import to_excel + to_excel.cache_clear() + cell.value = value + if bold: + cell.font = self.bold + if col_widths: + self.ws.column_dimensions[get_column_letter(col_idx)].width = col_widths[col_idx - 1] + self.row_idx += 1 + + def get_http_response(self, filename_base): + response = HttpResponse(save_virtual_workbook(self.wb), content_type=openxml_contenttype) + response['Content-Disposition'] = 'attachment; filename=%s_%s.xlsx' % ( + filename_base, date.strftime(date.today(), '%Y-%m-%d')) + return response + + +EXPORT_FIELDS = [ + # Student fields + ('ID externe', 'student__ext_id'), + ('Prénom', 'student__first_name'), ('Nom', 'student__last_name'), + ('Titre', 'student__gender'), + ('Classe', 'student__klass__name'), + ('Filière', 'student__klass__section__name'), + ('Rue élève', 'student__street'), + ('NPA_élève', 'student__pcode'), + ('Localité élève', 'student__city'), + ('Tél élève', 'student__tel'), + ('Email élève', 'student__email'), + ('Date de naissance', 'student__birth_date'), + ('No AVS', 'student__avs'), + # Stage fields + ('Nom du stage', 'availability__period__title'), + ('Début', 'availability__period__start_date'), ('Fin', 'availability__period__end_date'), + ('Remarques stage', 'comment'), + ('Prénom référent', 'referent__first_name'), ('Nom référent', 'referent__last_name'), + ('Courriel référent', 'referent__email'), + ('Institution', 'availability__corporation__name'), + ('ID externe Inst', 'availability__corporation__ext_id'), + ('Rue Inst', 'availability__corporation__street'), + ('NPA Inst', 'availability__corporation__pcode'), + ('Ville Inst', 'availability__corporation__city'), + ('Tél Inst', 'availability__corporation__tel'), + ('Domaine', 'availability__domain__name'), + ('Remarques Inst', 'availability__comment'), + ('Civilité contact', 'availability__contact__civility'), + ('Prénom contact', 'availability__contact__first_name'), + ('Nom contact', 'availability__contact__last_name'), + ('ID externe contact', 'availability__contact__ext_id'), + ('Tél contact', 'availability__contact__tel'), + ('Courriel contact', 'availability__contact__email'), + ('Courriel contact - copie', None), +] + + +NON_ATTR_EXPORT_FIELDS = [ + ('Filière', 'period__section__name'), + ('Nom du stage', 'period__title'), + ('Début', 'period__start_date'), ('Fin', 'period__end_date'), + ('Institution', 'corporation__name'), + ('Rue Inst', 'corporation__street'), + ('NPA Inst', 'corporation__pcode'), + ('Ville Inst', 'corporation__city'), + ('Tél Inst', 'corporation__tel'), + ('Domaine', 'domain__name'), + ('Remarques Inst', 'comment'), + ('Civilité contact', 'contact__civility'), + ('Prénom contact', 'contact__first_name'), + ('Nom contact', 'contact__last_name'), + ('Tél contact', 'contact__tel'), + ('Courriel contact', 'contact__email'), + ('Courriel contact - copie', None), +] + + +def stages_export(request, scope=None): + period_filter = request.GET.get('period') + non_attributed = bool(int(request.GET.get('non_attr', 0))) + + export_fields = OrderedDict(EXPORT_FIELDS) + contact_test_field = 'availability__contact__last_name' + corp_name_field = 'availability__corporation__name' + + if period_filter: + if non_attributed: + # Export non attributed availabilities for a specific period + query = Availability.objects.filter(period_id=period_filter, training__isnull=True) + export_fields = OrderedDict(NON_ATTR_EXPORT_FIELDS) + contact_test_field = 'contact__last_name' + corp_name_field = 'corporation__name' + else: + # Export trainings for a specific period + query = Training.objects.filter(availability__period_id=period_filter) + else: + if scope and scope == 'all': + # Export all trainings in the database + query = Training.objects.all() + else: + query = Training.objects.filter(availability__period__end_date__gt=school_year_start()) + + # Prepare "default" contacts (when not defined on training) + section_names = Section.objects.all().values_list('name', flat=True) + default_contacts = dict( + (c, {s: '' for s in section_names}) + for c in Corporation.objects.all().values_list('name', flat=True) + ) + always_ccs = dict( + (c, {s: [] for s in section_names}) + for c in Corporation.objects.all().values_list('name', flat=True) + ) + for contact in CorpContact.objects.filter(corporation__isnull=False + ).select_related('corporation' + ).prefetch_related('sections').order_by('corporation'): + for section in contact.sections.all(): + if not default_contacts[contact.corporation.name][section.name] or contact.is_main is True: + default_contacts[contact.corporation.name][section.name] = contact + if contact.always_cc: + always_ccs[contact.corporation.name][section.name].append(contact) + if contact.is_main: + for sname in section_names: + if not default_contacts[contact.corporation.name][sname]: + default_contacts[contact.corporation.name][sname] = contact + + export = OpenXMLExport('Stages') + export.write_line(export_fields.keys(), bold=True) # Headers + # Data + query_keys = [f for f in export_fields.values() if f is not None] + for line in query.values(*query_keys): + values = [] + for field in query_keys: + value = line[field] + if 'gender' in field: + value = {'F': 'Madame', 'M': 'Monsieur', '': ''}[value] + values.append(value) + if line[contact_test_field] is None: + # Use default contact + contact = default_contacts.get(line[corp_name_field], {}).get(line[export_fields['Filière']]) + if contact: + values = values[:-6] + [ + contact.civility, contact.first_name, contact.last_name, contact.ext_id, + contact.tel, contact.email + ] + if always_ccs[line[corp_name_field]].get(line[export_fields['Filière']]): + values.append("; ".join( + [c.email for c in always_ccs[line[corp_name_field]].get(line[export_fields['Filière']])] + )) + export.write_line(values) + + return export.get_http_response('stages_export') + + +def _ratio_Ede_Ase_Assc(): + # Spliting for unattribued periods + tot_edeps = Course.objects.filter(imputation='EDEps').aggregate(Sum('period'))['period__sum'] or 0 + tot_edepe = Course.objects.filter(imputation='EDEpe').aggregate(Sum('period'))['period__sum'] or 0 + edepe_ratio = 1 if tot_edepe + tot_edeps == 0 else tot_edepe / (tot_edepe + tot_edeps) + + tot_asefe = Course.objects.filter(imputation='ASEFE').aggregate(Sum('period'))['period__sum'] or 0 + tot_mpts = Course.objects.filter(imputation='MPTS').aggregate(Sum('period'))['period__sum'] or 0 + asefe_ratio = 1 if tot_asefe + tot_mpts == 0 else tot_asefe / (tot_asefe + tot_mpts) + + tot_asscfe = Course.objects.filter(imputation='ASSCFE').aggregate(Sum('period'))['period__sum'] or 0 + tot_mps = Course.objects.filter(imputation='MPS').aggregate(Sum('period'))['period__sum'] or 0 + asscfe_ratio = 1 if tot_asscfe + tot_mps == 0 else tot_asscfe / (tot_asscfe + tot_mps) + + return {'edepe':edepe_ratio, 'asefe':asefe_ratio, 'asscfe': asscfe_ratio} + + +def imputations_export(request): + IMPUTATIONS_EXPORT_FIELDS = [ + 'Nom', 'Prénom', 'Report passé', 'Ens', 'Discipline', + 'Accomp.', 'Discipline', 'Total payé', 'Indice', 'Taux', 'Report futur', + 'ASA', 'ASSC', 'ASE', 'MPTS', 'MPS', 'EDEpe', 'EDEps', 'EDS', 'CAS_FPP' + ] + + ratios = _ratio_Ede_Ase_Assc() + + export = OpenXMLExport('Imputations') + export.write_line(IMPUTATIONS_EXPORT_FIELDS, bold=True) # Headers + + for teacher in Teacher.objects.filter(archived=False): + activities, imputations = teacher.calc_imputations(ratios) + values = [ + teacher.last_name, teacher.first_name, teacher.previous_report, + activities['tot_ens'], 'Ens. prof.', activities['tot_mandats'] + activities['tot_formation'], + 'Accompagnement', activities['tot_paye'], 'Charge globale', + '{0:.2f}'.format(activities['tot_paye']/settings.GLOBAL_CHARGE_PERCENT), + teacher.next_report, + ] + values.extend(imputations.values()) + export.write_line(values) + + return export.get_http_response('Imputations_export') + + +def export_sap(request): + EXPORT_SAP_HEADERS = [ + 'PERNR', 'PERNOM', 'DEGDA', 'ENDDA', 'ZNOM', 'ZUND', + 'ZACT', 'ZBRA', 'ZOTP', 'ZCCO', 'ZORD', 'ZTAUX', + ] + MAPPING_OTP = { + 'ASAFE': 'CIFO01.03.02.03.01.02 - ASA EE', + 'ASEFE': 'CIFO01.03.02.04.01.02 - CFC ASE EE', + 'ASSCFE': 'CIFO01.03.02.04.02.02 - CFC ASSC EE', + 'EDEpe': 'CIFO01.03.02.07.01.01 - EDE prat. prof. PT', + 'EDEps': 'CIFO01.03.02.07.02.01 - EDE stages PT', + 'EDS': 'CIFO01.03.02.07.03.02 - EDS EE', + 'CAS_FPP': 'CIFO01.03.02.01.03 - Mandats divers (CAS FPP)', + 'MPTS' : 'CIFO01.04.03.06.02.01 - MPTS ASE', + 'MPS': 'CIFO01.04.03.06.03.01 - MPS Santé', + } + + ratios = _ratio_Ede_Ase_Assc() + + export = OpenXMLExport('Imputations') + export.write_line(EXPORT_SAP_HEADERS, bold=True) # Headers + start_date = '20.08.2018' + end_date = '19.08.2019' + indice = 'charge globale' + type_act = 'Ens. prof.' + branche = 'Ens. prof.' + centre_cout = '' + stat = '' + + for teacher in Teacher.objects.filter(archived=False): + activities, imputations = teacher.calc_imputations(ratios) + for key in imputations: + if imputations[key] > 0: + values = [ + teacher.ext_id, teacher.full_name, start_date, end_date, imputations[key], indice, type_act, + branche, MAPPING_OTP[key], centre_cout, stat, + round(imputations[key] / settings.GLOBAL_CHARGE_PERCENT, 2), + ] + export.write_line(values) + + # Previous report + values = [ + teacher.ext_id, teacher.full_name, start_date, end_date, teacher.previous_report, indice, type_act, + branche, 'Report précédent', centre_cout, stat, + round(teacher.previous_report / settings.GLOBAL_CHARGE_PERCENT, 2), + ] + export.write_line(values) + + # Next report + values = [ + teacher.ext_id, teacher.full_name, start_date, end_date, teacher.next_report, indice, type_act, + branche, 'Report suivant', centre_cout, stat, + round(teacher.next_report / settings.GLOBAL_CHARGE_PERCENT, 2), + ] + export.write_line(values) + return export.get_http_response('Export_SAP') + + +GENERAL_EXPORT_FIELDS = [ + ('Num_Ele', 'ext_id'), + ('Nom_Ele', 'last_name'), + ('Prenom_Ele', 'first_name'), + ('Genre_Ele', 'gender'), + ('Rue_Ele', 'street'), + ('NPA_Ele', 'pcode'), + ('Ville_Ele', 'city'), + ('DateNaissance_Ele', 'birth_date'), + ('NOAVS_Ele', 'avs'), + ('Canton_Ele', 'district'), + ('Email_Ele', 'email'), + ('Mobile_Ele', 'mobile'), + ('DispenseCG_Ele', 'dispense_ecg'), + ('DispenseEPS_Ele', 'dispense_eps'), + ('SoutienDYS_Ele', 'soutien_dys'), + + ('Classe_Ele', 'klass__name'), + ('Filiere_Ele', 'klass__section__name'), + ('MaitreDeClasseNom_Ele', 'klass__teacher__last_name'), + ('MaitreDeClassePrenom_Ele', 'klass__teacher__first_name'), + ('OptionASE_Ele', 'option_ase__name'), + + ('Num_Emp', 'corporation__ext_id'), + ('Nom_Emp', 'corporation__name'), + ('Rue_Emp', 'corporation__street'), + ('NPA_Emp', 'corporation__pcode'), + ('Ville_Emp', 'corporation__city'), + ('Canton_Emp', 'corporation__district'), + ('Secteur_Emp', 'corporation__sector'), + ('Type_EMP', 'corporation__typ'), + ('Tel_Emp', 'corporation__tel'), + + ('Num_Form', 'instructor__ext_id'), + ('Titre_Form', 'instructor__civility'), + ('Prenom_Form', 'instructor__first_name'), + ('Nom_Form', 'instructor__last_name'), + ('Tel_Form', 'instructor__tel'), + ('Email_Form', 'instructor__email'), + ('EmailCopie_Form', None), +] + + +def general_export(request): + """ + Export all current students data + """ + export_fields = OrderedDict(GENERAL_EXPORT_FIELDS) + export = OpenXMLExport('Exportation') + export.write_line(export_fields.keys(), bold=True) # Headers + # Data + query_keys = [f for f in export_fields.values() if f is not None] + query = Student.objects.filter(archived=False).order_by('klass__name', 'last_name', 'first_name') + for line in query.values(*query_keys): + values = [] + for field in query_keys: + if field == 'gender': + values.append(('Madame', 'Monsieur')[line[field] == 'M']) + elif field in ('dispense_ecg', 'dispense_eps', 'soutien_dys'): + values.append('Oui' if line[field] is True else '') + else: + values.append(line[field]) + export.write_line(values) + + return export.get_http_response('general_export') + + +ORTRA_EXPORT_FIELDS = [ + ('Num_Ele', 'ext_id'), + ('Nom_Ele', 'last_name'), + ('Prenom_Ele', 'first_name'), + ('Genre_Ele', 'gender'), + ('Rue_Ele', 'street'), + ('NPA_Ele', 'pcode'), + ('Ville_Ele', 'city'), + ('DateNaissance_Ele', 'birth_date'), + ('Email_Ele', 'email'), + ('Mobile_Ele', 'mobile'), + + ('Classe_Ele', 'klass__name'), + ('Filiere_Ele', 'klass__section__name'), + ('MaitreDeClasseNom_Ele', 'klass__teacher__last_name'), + ('MaitreDeClassePrenom_Ele', 'klass__teacher__first_name'), + ('OptionASE_Ele', 'option_ase__name'), + + ('Num_Emp', 'corporation__ext_id'), + ('Nom_Emp', 'corporation__name'), + ('Rue_Emp', 'corporation__street'), + ('NPA_Emp', 'corporation__pcode'), + ('Ville_Emp', 'corporation__city'), + ('Tel_Emp', 'corporation__tel'), + + ('Titre_Form', 'instructor__civility'), + ('Prenom_Form', 'instructor__first_name'), + ('Nom_Form', 'instructor__last_name'), + ('Tel_Form', 'instructor__tel'), + ('Email_Form', 'instructor__email'), +] + + +def ortra_export(request): + """ + Export students data from sections ASAFE, ASEFE and ASSCFE + """ + export_fields = OrderedDict(ORTRA_EXPORT_FIELDS) + export = OpenXMLExport('Exportation') + export.write_line(export_fields.keys(), bold=True) # Headers + # Data + query_keys = [f for f in export_fields.values() if f is not None] + query = Student.objects.filter(Q(klass__name__contains='ASAFE') | + Q(klass__name__contains='ASEFE') | + Q(klass__name__contains='ASSCFE'), + archived=False).order_by('klass__name', + 'last_name', + 'first_name') + + for line in query.values(*query_keys): + values = [] + for field in query_keys: + if field == 'gender': + values.append(('Madame', 'Monsieur')[line[field] == 'M']) + else: + values.append(line[field]) + export.write_line(values) + + return export.get_http_response('ortra_export') + + +def export_qualification_ede(request): + headers = [ + 'Classe', 'Etudiant-e', + 'Référent pratique', 'Résumé TD', 'Ens. référent', 'dernier RDV', + 'Mentor', + 'Session', + 'Titre TD', + 'Exp_int.', + 'Expert ext. Civilité', 'Expert ext. Nom', 'Expert ext. Adresse', 'Expert ext. Localité', + 'Date', 'Salle', 'Note', + ] + + export = OpenXMLExport('Expor_Qualif_EDE') + export.write_line(headers, bold=True) + + # Data + for student in Student.objects.filter(klass__name__startswith='3EDE', archived=False + ).select_related('klass', 'referent', 'training_referent', 'mentor', 'expert', 'internal_expert', + ).order_by('klass__name', 'last_name'): + values = [ + student.klass.name, + student.full_name, + student.training_referent.full_name if student.training_referent else '', + student.subject, + student.referent.full_name if student.referent else '', + student.last_appointment, + student.mentor.full_name if student.mentor else '', + str(student.session), + student.title, + student.internal_expert.full_name if student.internal_expert else '', + student.expert.civility if student.expert else '', + student.expert.full_name if student.expert else '', + student.expert.street if student.expert else '', + student.expert.pcode_city if student.expert else '', + student.date_exam, + student.room, + student.mark, + ] + export.write_line(values) + + return export.get_http_response('Export_qualif_EDE')