# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # ############################################################################## import re import requests from datetime import datetime from flask import url_for from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed, FileRequired from markupsafe import Markup from sqlalchemy import text from wtforms import ( StringField, IntegerField, SubmitField, TextAreaField, SelectField, HiddenField, SelectMultipleField, DateField, BooleanField, FieldList, FormField, BooleanField, ) from wtforms.validators import ( ValidationError, DataRequired, Email, Optional, NumberRange, ) from wtforms.widgets import ListWidget, CheckboxInput from app.entreprises.models import ( Entreprise, EntrepriseCorrespondant, EntreprisePreferences, EntrepriseSite, EntrepriseTaxeApprentissage, ) from app import db from app.models import Identite, Departement from app.auth.models import User from app.entreprises import SIRET_PROVISOIRE_START CHAMP_REQUIS = "Ce champ est requis" SUBMIT_MARGE = {"style": "margin-bottom: 10px;"} def _build_string_field(label, required=True, render_kw=None): if required: return StringField( label, validators=[DataRequired(message=CHAMP_REQUIS)], render_kw=render_kw, ) else: return StringField(label, validators=[Optional()], render_kw=render_kw) class EntreprisesFilterForm(FlaskForm): active = BooleanField("Toutes les entreprises") association = BooleanField("Seulement les associations partenaires") siret_provisoire = BooleanField("Seulement SIRET provisoire") class EntrepriseCreationForm(FlaskForm): siret = StringField( "SIRET", validators=[Optional()], render_kw={"placeholder": "Numéro composé de 14 chiffres"}, description="Laissez vide pour générer un SIRET provisoire", ) association = BooleanField("Association") nom_entreprise = _build_string_field("Nom de l'entreprise (*)") adresse = _build_string_field("Adresse de l'entreprise (*)") codepostal = _build_string_field("Code postal de l'entreprise (*)") ville = _build_string_field("Ville de l'entreprise (*)") pays = _build_string_field("Pays de l'entreprise", required=False) civilite = SelectField( "Civilité du correspondant", choices=[("H", "Monsieur"), ("F", "Madame")], validators=[DataRequired(message=CHAMP_REQUIS)], ) nom_correspondant = _build_string_field("Nom du correspondant", required=False) prenom_correspondant = _build_string_field( "Prénom du correspondant", required=False ) telephone = _build_string_field("Téléphone du correspondant", required=False) mail = StringField( "Mail du correspondant", validators=[Optional(), Email(message="Adresse e-mail invalide")], ) poste = _build_string_field("Poste du correspondant", required=False) service = _build_string_field("Service du correspondant", required=False) origine = _build_string_field("Origine du correspondant", required=False) notes = _build_string_field("Notes sur le correspondant", required=False) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False if EntreprisePreferences.get_check_siret() and self.siret.data != "": siret_data = self.siret.data.strip().replace(" ", "") self.siret.data = siret_data if re.match("^\d{14}$", siret_data) is None: self.siret.errors.append("Format incorrect") validate = False else: try: req = requests.get( f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret_data}" ) if req.status_code != 200: self.siret.errors.append("SIRET inexistant") validate = False except requests.ConnectionError: self.siret.errors.append( "Impossible de vérifier l'existance du SIRET" ) validate = False entreprise = Entreprise.query.filter_by(siret=siret_data).first() if entreprise is not None: if entreprise.visible is True: lien = f"ici" self.siret.errors.append( Markup( f"Entreprise déjà présent, lien vers la fiche : {lien}" ) ) validate = False else: self.siret.errors.append("Entreprise en phase de validation") validate = False if ( self.nom_correspondant.data.strip() or self.prenom_correspondant.data.strip() or self.telephone.data.strip() or self.mail.data.strip() or self.poste.data.strip() or self.service.data.strip() or self.origine.data.strip() or self.notes.data.strip() ): if not self.nom_correspondant.data.strip(): self.nom_correspondant.errors.append("Ce champ est requis") validate = False if not self.prenom_correspondant.data.strip(): self.prenom_correspondant.errors.append("Ce champ est requis") validate = False if not self.telephone.data.strip() and not self.mail.data.strip(): msg = "Saisir un moyen de contact (mail ou téléphone)" self.telephone.errors.append(msg) self.mail.errors.append(msg) validate = False return validate class EntrepriseModificationForm(FlaskForm): siret = StringField("SIRET (*)") new_siret = StringField( "Modification du SIRET provisoire (*)", description="Activé uniquement pour les entreprises avec SIRET provisoire", ) association = BooleanField("Association") nom = _build_string_field("Nom de l'entreprise (*)") adresse = _build_string_field("Adresse (*)") codepostal = _build_string_field("Code postal (*)") ville = _build_string_field("Ville (*)") pays = _build_string_field("Pays", required=False) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.siret.render_kw = {"disabled": ""} if self.siret.data.startswith(SIRET_PROVISOIRE_START) is True: self.new_siret.validators = [Optional()] else: self.new_siret.render_kw = {"disabled": ""} def validate_new_siret(self, new_siret): if EntreprisePreferences.get_check_siret() and new_siret.data is not None: siret_data = new_siret.data.strip().replace(" ", "") self.new_siret.data = siret_data if re.match("^\d{14}$", siret_data) is None: raise ValidationError("Format incorrect") else: try: req = requests.get( f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret_data}" ) if req.status_code != 200: raise ValidationError("SIRET inexistant") except requests.ConnectionError: raise ValidationError("Impossible de vérifier l'existance du SIRET") entreprise = Entreprise.query.filter_by(siret=siret_data).first() if entreprise is not None: if entreprise.visible is True: lien = f'ici' raise ValidationError( Markup( f"Entreprise déjà présent, lien vers la fiche : {lien}" ) ) else: raise ValidationError("Entreprise en phase de validation") class SiteCreationForm(FlaskForm): hidden_entreprise_id = HiddenField() nom = _build_string_field("Nom du site (*)") adresse = _build_string_field("Adresse (*)") codepostal = _build_string_field("Code postal (*)") ville = _build_string_field("Ville (*)") pays = _build_string_field("Pays", required=False) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False site = EntrepriseSite.query.filter_by( entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data ).first() if site is not None: self.nom.errors.append("Ce site existe déjà (même nom)") validate = False return validate class SiteModificationForm(FlaskForm): hidden_entreprise_id = HiddenField() hidden_site_id = HiddenField() nom = _build_string_field("Nom du site (*)") adresse = _build_string_field("Adresse (*)") codepostal = _build_string_field("Code postal (*)") ville = _build_string_field("Ville (*)") pays = _build_string_field("Pays", required=False) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False site = EntrepriseSite.query.filter( EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data, EntrepriseSite.id != self.hidden_site_id.data, EntrepriseSite.nom == self.nom.data, ).first() if site is not None: self.nom.errors.append("Ce site existe déjà (même nom)") validate = False return validate class MultiCheckboxField(SelectMultipleField): widget = ListWidget(prefix_label=False) option_widget = CheckboxInput() class OffreCreationForm(FlaskForm): hidden_entreprise_id = HiddenField() intitule = _build_string_field("Intitulé (*)") description = TextAreaField( "Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) type_offre = SelectField( "Type de l'offre (*)", choices=[("Stage"), ("Alternance")], validators=[DataRequired(message=CHAMP_REQUIS)], ) missions = TextAreaField( "Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) duree = _build_string_field("Durée (*)") depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int) expiration_date = DateField("Date expiration", validators=[Optional()]) correspondant = SelectField("Correspondant à contacté", validators=[Optional()]) fichier = FileField( "Fichier", validators=[ Optional(), FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"), ], ) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.correspondant.choices = [("", "")] + [ (correspondant.id, f"{correspondant.nom} {correspondant.prenom}") for correspondant in db.session.query(EntrepriseCorrespondant) .join(EntrepriseSite, EntrepriseCorrespondant.site_id == EntrepriseSite.id) .filter(EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data) .all() ] self.depts.choices = [ (dept.id, dept.acronym) for dept in Departement.query.all() ] def validate(self): validate = True if not FlaskForm.validate(self): validate = False if len(self.depts.data) < 1: self.depts.errors.append("Choisir au moins un département") validate = False return validate class OffreModificationForm(FlaskForm): hidden_entreprise_id = HiddenField() intitule = _build_string_field("Intitulé (*)") description = TextAreaField( "Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) type_offre = SelectField( "Type de l'offre (*)", choices=[("Stage"), ("Alternance")], validators=[DataRequired(message=CHAMP_REQUIS)], ) missions = TextAreaField( "Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) duree = _build_string_field("Durée (*)") depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int) expiration_date = DateField("Date expiration", validators=[Optional()]) correspondant = SelectField("Correspondant à contacté", validators=[Optional()]) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.correspondant.choices = [("", "")] + [ (correspondant.id, f"{correspondant.nom} {correspondant.prenom}") for correspondant in db.session.query(EntrepriseCorrespondant) .join(EntrepriseSite, EntrepriseCorrespondant.site_id == EntrepriseSite.id) .filter(EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data) .all() ] self.depts.choices = [ (dept.id, dept.acronym) for dept in Departement.query.all() ] def validate(self): validate = True if not FlaskForm.validate(self): validate = False if len(self.depts.data) < 1: self.depts.errors.append("Choisir au moins un département") validate = False return validate class CorrespondantCreationForm(FlaskForm): civilite = SelectField( "Civilité (*)", choices=[("H", "Monsieur"), ("F", "Madame")], validators=[DataRequired(message=CHAMP_REQUIS)], render_kw={"class": "form-control"}, ) nom = StringField( "Nom (*)", validators=[DataRequired('Le champ "Nom" est requis')], render_kw={"class": "form-control"}, ) prenom = StringField( "Prénom (*)", validators=[DataRequired('Le champ "Prénom" est requis')], render_kw={"class": "form-control"}, ) telephone = _build_string_field( "Téléphone (*)", required=False, render_kw={"class": "form-control"} ) mail = StringField( "Mail (*)", validators=[Optional(), Email(message="Adresse e-mail invalide")], render_kw={"class": "form-control"}, ) poste = _build_string_field( "Poste", required=False, render_kw={"class": "form-control"} ) service = _build_string_field( "Service", required=False, render_kw={"class": "form-control"} ) origine = _build_string_field( "Origine", required=False, render_kw={"class": "form-control"} ) notes = _build_string_field( "Notes", required=False, render_kw={"class": "form-control"} ) def validate(self): validate = True if not FlaskForm.validate(self): validate = False if not self.telephone.data and not self.mail.data: msg = "Saisir un moyen de contact (mail ou téléphone)" self.telephone.errors.append(msg) validate = False return validate class CorrespondantsCreationForm(FlaskForm): hidden_site_id = HiddenField() correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False correspondant_list = [] for entry in self.correspondants.entries: if entry.nom.data and entry.prenom.data: if ( entry.nom.data.strip(), entry.prenom.data.strip(), ) in correspondant_list: entry.nom.errors.append( "Vous avez saisi 2 fois le même nom et prenom" ) entry.prenom.errors.append("") validate = False correspondant_list.append( (entry.nom.data.strip(), entry.prenom.data.strip()) ) correspondant = EntrepriseCorrespondant.query.filter_by( site_id=self.hidden_site_id.data, nom=entry.nom.data, prenom=entry.prenom.data, ).first() if correspondant is not None: entry.nom.errors.append( "Ce correspondant existe déjà (même nom et prénom)" ) entry.prenom.errors.append("") validate = False return validate class CorrespondantModificationForm(FlaskForm): hidden_correspondant_id = HiddenField() hidden_site_id = HiddenField() civilite = SelectField( "Civilité (*)", choices=[("H", "Monsieur"), ("F", "Madame")], validators=[DataRequired(message=CHAMP_REQUIS)], ) nom = _build_string_field("Nom (*)") prenom = _build_string_field("Prénom (*)") telephone = _build_string_field("Téléphone (*)", required=False) mail = StringField( "Mail (*)", validators=[Optional(), Email(message="Adresse e-mail invalide")], ) poste = _build_string_field("Poste", required=False) service = _build_string_field("Service", required=False) origine = _build_string_field("Origine", required=False) notes = _build_string_field("Notes", required=False) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False correspondant = EntrepriseCorrespondant.query.filter( EntrepriseCorrespondant.id != self.hidden_correspondant_id.data, EntrepriseCorrespondant.site_id == self.hidden_site_id.data, EntrepriseCorrespondant.nom == self.nom.data, EntrepriseCorrespondant.prenom == self.prenom.data, ).first() if correspondant is not None: self.nom.errors.append("Ce correspondant existe déjà (même nom et prénom)") self.prenom.errors.append("") validate = False if not self.telephone.data and not self.mail.data: msg = "Saisir un moyen de contact (mail ou téléphone)" self.telephone.errors.append(msg) self.mail.errors.append(msg) validate = False return validate class ContactCreationForm(FlaskForm): date = _build_string_field( "Date (*)", render_kw={"type": "datetime-local"}, ) utilisateur = _build_string_field( "Utilisateur (*)", render_kw={"placeholder": "Tapez le nom de l'utilisateur"}, ) notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)]) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate_utilisateur(self, utilisateur): utilisateur_data = self.utilisateur.data.upper().strip() stm = text( "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data" ) utilisateur = ( User.query.from_statement(stm) .params(utilisateur_data=utilisateur_data) .first() ) if utilisateur is None: raise ValidationError("Champ incorrect (selectionnez dans la liste)") class ContactModificationForm(FlaskForm): date = _build_string_field( "Date (*)", render_kw={"type": "datetime-local"}, ) utilisateur = _build_string_field( "Utilisateur (*)", render_kw={"placeholder": "Tapez le nom de l'utilisateur"}, ) notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)]) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def validate_utilisateur(self, utilisateur): utilisateur_data = self.utilisateur.data.upper().strip() stm = text( "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data" ) utilisateur = ( User.query.from_statement(stm) .params(utilisateur_data=utilisateur_data) .first() ) if utilisateur is None: raise ValidationError("Champ incorrect (selectionnez dans la liste)") class StageApprentissageCreationForm(FlaskForm): etudiant = _build_string_field( "Étudiant (*)", render_kw={"placeholder": "Tapez le nom de l'étudiant"}, ) type_offre = SelectField( "Type de l'offre (*)", choices=[("Stage"), ("Alternance")], validators=[DataRequired(message=CHAMP_REQUIS)], ) date_debut = DateField( "Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) date_fin = DateField( "Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) notes = TextAreaField("Notes") submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False if ( self.date_debut.data and self.date_fin.data and self.date_debut.data > self.date_fin.data ): self.date_debut.errors.append("Les dates sont incompatibles") self.date_fin.errors.append("Les dates sont incompatibles") validate = False return validate def validate_etudiant(self, etudiant): etudiant_data = etudiant.data.upper().strip() stm = text( "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom" ) etudiant = ( Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first() ) if etudiant is None: raise ValidationError("Champ incorrect (selectionnez dans la liste)") class StageApprentissageModificationForm(FlaskForm): etudiant = _build_string_field( "Étudiant (*)", render_kw={"placeholder": "Tapez le nom de l'étudiant"}, ) type_offre = SelectField( "Type de l'offre (*)", choices=[("Stage"), ("Alternance")], validators=[DataRequired(message=CHAMP_REQUIS)], ) date_debut = DateField( "Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) date_fin = DateField( "Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) notes = TextAreaField("Notes") submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False if ( self.date_debut.data and self.date_fin.data and self.date_debut.data > self.date_fin.data ): self.date_debut.errors.append("Les dates sont incompatibles") self.date_fin.errors.append("Les dates sont incompatibles") validate = False return validate def validate_etudiant(self, etudiant): etudiant_data = etudiant.data.upper().strip() stm = text( "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom" ) etudiant = ( Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first() ) if etudiant is None: raise ValidationError("Champ incorrect (selectionnez dans la liste)") class TaxeApprentissageForm(FlaskForm): hidden_entreprise_id = HiddenField() annee = IntegerField( "Année (*)", validators=[ DataRequired(message=CHAMP_REQUIS), NumberRange( min=1900, max=int(datetime.now().strftime("%Y")), message=f"L'année doit être inférieure ou égale à {int(datetime.now().strftime('%Y'))}", ), ], default=int(datetime.now().strftime("%Y")), ) montant = IntegerField( "Montant (*)", validators=[ DataRequired(message=CHAMP_REQUIS), NumberRange( min=1, message="Le montant doit être supérieur à 0", ), ], default=1, ) notes = TextAreaField("Notes") submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): validate = True if not FlaskForm.validate(self): validate = False taxe = EntrepriseTaxeApprentissage.query.filter_by( entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data ).first() if taxe is not None: self.annee.errors.append( "Une taxe d'apprentissage a déjà été versé pour cette année" ) validate = False return validate class TaxeApprentissageModificationForm(FlaskForm): annee = IntegerField("Année (*)") montant = IntegerField( "Montant (*)", validators=[ DataRequired(message=CHAMP_REQUIS), NumberRange( min=1, message="Le montant doit être supérieur à 0", ), ], default=1, ) notes = TextAreaField("Notes") submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.annee.render_kw = {"disabled": ""} class EnvoiOffreForm(FlaskForm): responsables = FieldList( StringField( "Responsable (*)", render_kw={ "placeholder": "Tapez le nom du responsable de formation", "class": "form-control", }, ), min_entries=1, ) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): validate = True list_select = True if not FlaskForm.validate(self): validate = False for entry in self.responsables.entries: if entry.data: responsable_data = entry.data.upper().strip() stm = text( "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data" ) responsable = ( User.query.from_statement(stm) .params(responsable_data=responsable_data) .first() ) if responsable is None: validate, list_select = False, False if list_select is False: self.responsables.errors.append( "Champ incorrect (selectionnez dans la liste)" ) return validate class AjoutFichierForm(FlaskForm): fichier = FileField( "Fichier (*)", validators=[ FileRequired(message=CHAMP_REQUIS), FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"), ], ) submit = SubmitField("Ajouter", render_kw=SUBMIT_MARGE) class SuppressionConfirmationForm(FlaskForm): submit = SubmitField("Supprimer", render_kw=SUBMIT_MARGE) class DesactivationConfirmationForm(FlaskForm): notes_active = TextAreaField("Notes sur la désactivation", validators=[Optional()]) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) class ActivationConfirmationForm(FlaskForm): submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) class ValidationConfirmationForm(FlaskForm): submit = SubmitField("Valider", render_kw=SUBMIT_MARGE) class ImportForm(FlaskForm): fichier = FileField( "Fichier (*)", validators=[ FileRequired(message=CHAMP_REQUIS), FileAllowed(["xlsx"], "Fichier .xlsx uniquement"), ], ) submit = SubmitField("Importer", render_kw=SUBMIT_MARGE) class PreferencesForm(FlaskForm): mail_entreprise = StringField( "Mail notifications", validators=[Optional(), Email(message="Adresse e-mail invalide")], description="utilisé pour envoi mail notification application relations entreprises", ) check_siret = BooleanField("Vérification SIRET") submit = SubmitField("Valider", render_kw=SUBMIT_MARGE)