diff --git a/.gitignore b/.gitignore index 87df45de6..b60367ec8 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,6 @@ Thumbs.db .idea/ copy + +# Symlinks static ScoDoc +app/static/links/[0-9]*.*[0-9] diff --git a/app/api/departements.py b/app/api/departements.py index fca5d6a82..853dc7120 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -153,4 +153,4 @@ def liste_semestres_courant(dept_ident: str): FormSemestre.date_fin >= app.db.func.now(), ) - return jsonify([d.to_dict() for d in formsemestres]) + return jsonify([d.to_dict(convert_parcours=True) for d in formsemestres]) diff --git a/app/entreprises/__init__.py b/app/entreprises/__init__.py index 7f100d51e..44fd128e7 100644 --- a/app/entreprises/__init__.py +++ b/app/entreprises/__init__.py @@ -1,6 +1,7 @@ """entreprises.__init__ """ +from datetime import datetime from flask import Blueprint from app.scodoc import sco_etud from app.auth.models import User @@ -47,4 +48,12 @@ def get_civilité(civ): return "Madame" +@bp.app_template_filter() +def check_taxe_now(taxes): + for taxe in taxes: + if taxe.annee == int(datetime.now().strftime("%Y")): + return True + return False + + from app.entreprises import routes diff --git a/app/entreprises/app_relations_entreprises.py b/app/entreprises/app_relations_entreprises.py index 95f1264cf..b5b14dab7 100644 --- a/app/entreprises/app_relations_entreprises.py +++ b/app/entreprises/app_relations_entreprises.py @@ -30,6 +30,7 @@ import re import requests import glob +from flask import flash from flask_login import current_user from app.entreprises.models import ( @@ -38,14 +39,61 @@ from app.entreprises.models import ( EntrepriseOffre, EntrepriseOffreDepartement, EntreprisePreferences, + EntrepriseSite, + EntrepriseHistorique, ) - -from app import email +from app import email, db from app.scodoc import sco_preferences +from app.scodoc import sco_excel from app.models import Departement from app.scodoc.sco_permissions import Permission +ENTREPRISES_KEYS = [ + "siret", + "nom_entreprise", + "adresse", + "ville", + "code_postal", + "pays", +] +SITES_KEYS = [ + [ + "siret_entreprise", + "id_site", + "nom_site", + "adresse", + "ville", + "code_postal", + "pays", + ], + [ + "civilite", + "nom", + "prenom", + "telephone", + "mail", + "poste", + "service", + "origine", + "notes", + ], +] +CORRESPONDANTS_KEYS = [ + "id", + "civilite", + "nom", + "prenom", + "telephone", + "mail", + "poste", + "service", + "origine", + "notes", + "nom_site", +] + + def get_depts(): """ Retourne une liste contenant les l'id des départements des roles de l'utilisateur courant @@ -58,7 +106,7 @@ def get_depts(): return depts -def get_dept_id_by_acronym(acronym): +def get_dept_id_by_acronym(acronym: str): """ Retourne l'id d'un departement a l'aide de son acronym """ @@ -68,7 +116,14 @@ def get_dept_id_by_acronym(acronym): return None -def check_offre_depts(depts, offre_depts): +def get_dept_acronym_by_id(id: int): + dept = Departement.query.filter_by(id=id).first() + if dept is not None: + return dept.acronym + return None + + +def check_offre_depts(depts: list, offre_depts: list): """ Retourne vrai si l'utilisateur a le droit de visibilité sur l'offre """ @@ -107,7 +162,7 @@ def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list): return None -def send_email_notifications_entreprise(subject, entreprise: Entreprise): +def send_email_notifications_entreprise(subject: str, entreprise: Entreprise): txt = [ "Une entreprise est en attente de validation", "Entreprise:", @@ -128,69 +183,168 @@ def send_email_notifications_entreprise(subject, entreprise: Entreprise): return txt -def verif_correspondant_data(correspondant_data): +def get_excel_book_are(export: bool = False): """ - Verifie les données d'une ligne Excel (correspondant) - correspondant_data[0]: civilite - correspondant_data[1]: nom - correspondant_data[2]: prenom - correspondant_data[3]: telephone - correspondant_data[4]: mail - correspondant_data[5]: poste - correspondant_data[6]: service - correspondant_data[7]: origine - correspondant_data[8]: notes - correspondant_data[9]: entreprise_siret + Retourne un Excel avec les 3 feuilles "Entreprises", "Sites" et "Correspondants" """ - # champs obligatoires - if ( - correspondant_data[0].strip() == "" - or correspondant_data[1].strip() == "" - or correspondant_data[2].strip() == "" - or correspondant_data[9].strip() == "" - ): + entreprises_titles = ENTREPRISES_KEYS[:] + sites_titles = SITES_KEYS[:] + correspondants_titles = CORRESPONDANTS_KEYS[:] + wb = sco_excel.ScoExcelBook() + ws1 = wb.create_sheet("Entreprises") + ws1.append_row( + [ + ws1.make_cell(it, style) + for (it, style) in zip( + entreprises_titles, + [sco_excel.excel_make_style(bold=True)] * len(entreprises_titles), + ) + ] + ) + ws2 = wb.create_sheet("Sites") + ws2.append_row( + [ + ws2.make_cell(it, style) + for (it, style) in zip( + sites_titles[0], + [sco_excel.excel_make_style(bold=True)] * len(sites_titles[0]), + ) + ] + + [ + ws2.make_cell(it, style) + for (it, style) in zip( + sites_titles[1], + [sco_excel.excel_make_style(bold=True, color=sco_excel.COLORS.RED)] + * len(sites_titles[1]), + ) + ] + ) + ws3 = wb.create_sheet("Correspondants") + ws3.append_row( + [ + ws3.make_cell(it, style) + for (it, style) in zip( + correspondants_titles, + [sco_excel.excel_make_style(bold=True)] * len(correspondants_titles), + ) + ] + ) + if export: + entreprises = Entreprise.query.filter_by(visible=True).all() + sites = ( + db.session.query(EntrepriseSite) + .join(Entreprise, EntrepriseSite.entreprise_id == Entreprise.id) + .filter_by(visible=True) + .all() + ) + correspondants = ( + db.session.query(EntrepriseCorrespondant) + .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id) + .filter_by(visible=True) + .all() + ) + entreprises_lines = [ + [entreprise.to_dict().get(k, "") for k in ENTREPRISES_KEYS] + for entreprise in entreprises + ] + sites_lines = [ + [site.to_dict().get(k, "") for k in SITES_KEYS[0]] for site in sites + ] + correspondants_lines = [ + [correspondant.to_dict().get(k, "") for k in CORRESPONDANTS_KEYS] + for correspondant in correspondants + ] + for line in entreprises_lines: + cells = [] + for it in line: + cells.append(ws1.make_cell(it)) + ws1.append_row(cells) + for line in sites_lines: + cells = [] + for it in line: + cells.append(ws2.make_cell(it)) + ws2.append_row(cells) + for line in correspondants_lines: + cells = [] + for it in line: + cells.append(ws3.make_cell(it)) + ws3.append_row(cells) + return wb + + +def check_entreprises_import(m): + """ + Verifie la feuille Excel "Entreprises" de l'importation données + """ + entreprises_import = [] + siret_list = [] + ligne = 1 + if m[0] != ENTREPRISES_KEYS: + flash( + f'Veuillez utilisez la feuille excel à remplir (Feuille "Entreprises", ligne {ligne})' + ) return False + l = list_to_dict(m) + for entreprise_data in l: + ligne += 1 + entreprise_data["siret"] = entreprise_data["siret"].replace(" ", "") + if ( + check_entreprise_import(entreprise_data) + and entreprise_data["siret"] not in siret_list + ): + siret_list.append(entreprise_data["siret"]) + entreprise = Entreprise.query.filter_by( + siret=entreprise_data["siret"], visible=True + ).first() + if entreprise is None: + entreprise_import = Entreprise( + siret=entreprise_data["siret"], + nom=entreprise_data["nom_entreprise"], + adresse=entreprise_data["adresse"], + ville=entreprise_data["ville"], + codepostal=entreprise_data["code_postal"], + pays=entreprise_data["pays"] + if entreprise_data["pays"] + else "FRANCE", + visible=True, + ) + entreprises_import.append(entreprise_import) + else: + entreprise.nom = entreprise_data["nom_entreprise"] + entreprise.adresse = entreprise_data["adresse"] + entreprise.ville = entreprise_data["ville"] + entreprise.codepostal = entreprise_data["code_postal"] + entreprise.pays = ( + entreprise_data["pays"] if entreprise_data["pays"] else "FRANCE" + ) + else: + flash( + f'Erreur lors de l\'importation (Feuille "Entreprises", ligne {ligne})' + ) + return False - # civilite entre H ou F - if correspondant_data[0].strip() not in ["H", "F"]: - return False + if len(entreprises_import) > 0: + log = EntrepriseHistorique( + authenticated_user=current_user.user_name, + text=f"Importation de {len(entreprises_import)} entreprise(s)", + ) + db.session.add(log) - # entreprise_id existant - entreprise = Entreprise.query.filter_by(siret=correspondant_data[9].strip()).first() - if entreprise is None: - return False - - # correspondant possède le meme nom et prénom dans la meme entreprise - correspondant = EntrepriseCorrespondant.query.filter_by( - nom=correspondant_data[1].strip(), - prenom=correspondant_data[2].strip(), - entreprise_id=entreprise.id, - ).first() - if correspondant is not None: - return False - - if ( - correspondant_data[3].strip() == "" and correspondant_data[4].strip() == "" - ): # 1 moyen de contact - return False - - return True + return entreprises_import -def verif_entreprise_data(entreprise_data): +def check_entreprise_import(entreprise_data): """ Verifie les données d'une ligne Excel (entreprise) """ + champs_obligatoires = ["siret", "nom_entreprise", "adresse", "ville", "code_postal"] + for key, value in entreprise_data.items(): # champs obligatoires + if key in champs_obligatoires and value == "": + return False + + siret = entreprise_data["siret"] + if EntreprisePreferences.get_check_siret(): - for data in entreprise_data: # champs obligatoires - if data.strip() == "": - return False - else: - for data in entreprise_data[1:]: # champs obligatoires - if data.strip() == "": - return False - if EntreprisePreferences.get_check_siret(): - siret = entreprise_data[0].strip().replace(" ", "") # vérification sur le siret if re.match("^\d{14}$", siret) is None: return False try: @@ -201,7 +355,210 @@ def verif_entreprise_data(entreprise_data): return False except requests.ConnectionError: return False - entreprise = Entreprise.query.filter_by(siret=siret).first() - if entreprise is not None: + return True + + +def check_sites_import(m): + """ + Verifie la feuille Excel "Sites" de l'importation données + """ + sites_import = [] + correspondants_import = [] + ligne = 1 + if m[0] != sum(SITES_KEYS, []): + flash( + f'Veuillez utilisez la feuille excel à remplir (Feuille "Sites", ligne {ligne})' + ) + return False, False + l = list_to_dict(m) + for site_data in l: + ligne += 1 + site_data["siret_entreprise"] = site_data["siret_entreprise"].replace(" ", "") + if check_site_import(site_data): + entreprise = Entreprise.query.filter_by( + siret=site_data["siret_entreprise"], visible=True + ).first() + if site_data["id_site"] == "": + site_import = EntrepriseSite( + entreprise_id=entreprise.id, + nom=site_data["nom_site"], + adresse=site_data["adresse"], + codepostal=site_data["code_postal"], + ville=site_data["ville"], + pays=site_data["pays"], + ) + if site_data["civilite"] == "": + sites_import.append(site_import) + else: + correspondant_import = EntrepriseCorrespondant( + entreprise_id=entreprise.id, + civilite=site_data["civilite"], + nom=site_data["nom"], + prenom=site_data["prenom"], + telephone=site_data["telephone"], + mail=site_data["mail"], + poste=site_data["poste"], + service=site_data["service"], + origine=site_data["origine"], + notes=site_data["notes"], + ) + sites_import.append(site_import) + correspondants_import.append([site_import, correspondant_import]) + else: + site = EntrepriseSite.query.filter_by(id=site_data["id_site"]).first() + site.nom = site_data["nom_site"] + site.adresse = site_data["adresse"] + site.codepostal = site_data["code_postal"] + site.ville = site_data["ville"] + site.pays = site_data["pays"] + + if site_data["civilite"] != "": + correspondant_import = EntrepriseCorrespondant( + entreprise_id=entreprise.id, + site_id=site.id, + civilite=site_data["civilite"], + nom=site_data["nom"], + prenom=site_data["prenom"], + telephone=site_data["telephone"], + mail=site_data["mail"], + poste=site_data["poste"], + service=site_data["service"], + origine=site_data["origine"], + notes=site_data["notes"], + ) + correspondants_import.append([None, correspondant_import]) + else: + flash(f'Erreur lors de l\'importation (Feuille "Sites", ligne {ligne})') + return False, False + + if len(sites_import) > 0: + log = EntrepriseHistorique( + authenticated_user=current_user.user_name, + text=f"Importation de {len(sites_import)} site(s)", + ) + db.session.add(log) + + if len(correspondants_import) > 0: + log = EntrepriseHistorique( + authenticated_user=current_user.user_name, + text=f"Importation de {len(correspondants_import)} correspondant(s)", + ) + db.session.add(log) + + return sites_import, correspondants_import + + +def check_site_import(site_data): + """ + Verifie les données d'une ligne Excel (sites) + """ + champs_obligatoires = [ + "siret_entreprise", + "nom_site", + "adresse", + "ville", + "code_postal", + "pays", + ] + for key, value in site_data.items(): # champs obligatoires + if key in champs_obligatoires and value == "": + return False + + if site_data["civilite"] != "": + if check_correspondant_import(site_data) is False: + return False + + entreprise = Entreprise.query.filter_by( + siret=site_data["siret_entreprise"], visible=True + ).first() + if entreprise is None: + return False + + site = EntrepriseSite.query.filter_by(nom=site_data["nom_site"]).first() + if site_data["id_site"] == "" and site is not None: + return False + + return True + + +def check_correspondants_import(m): + ligne = 1 + if m[0] != CORRESPONDANTS_KEYS: + flash( + f'Veuillez utilisez la feuille excel à remplir (Feuille "Correspondants", ligne {ligne})' + ) + return False + l = list_to_dict(m) + for correspondant_data in l: + ligne += 1 + if correspondant_data["id"] == "": + flash( + f'Erreur lors de l\'importation (Feuille "Correspondants", ligne {ligne})' + ) + return False + correspondant = EntrepriseCorrespondant.query.filter_by( + id=correspondant_data["id"] + ).first() + if correspondant is not None and check_correspondant_import(correspondant_data): + correspondant.civilite = correspondant_data["civilite"] + correspondant.nom = correspondant_data["nom"] + correspondant.prenom = correspondant_data["prenom"] + correspondant.telephone = correspondant_data["telephone"] + correspondant.mail = correspondant_data["mail"] + correspondant.poste = correspondant_data["poste"] + correspondant.service = correspondant_data["service"] + correspondant.origine = correspondant_data["origine"] + correspondant.notes = correspondant_data["notes"] + else: + flash( + f'Erreur lors de l\'importation (Feuille "Correspondants", ligne {ligne})' + ) return False return True + + +def check_correspondant_import(correspondant_data): + """ + Verifie les données d'une ligne Excel (correspondant) + """ + champs_obligatoires = ["civilite", "nom", "prenom"] + for key, value in correspondant_data.items(): # champs obligatoires + if key in champs_obligatoires and value == "": + return False + + # civilite entre H ou F + if correspondant_data["civilite"] not in ["H", "F"]: + return False + + if ( + correspondant_data["telephone"] == "" and correspondant_data["mail"] == "" + ): # 1 moyen de contact + return False + + if "siret_entreprise" in correspondant_data: + # entreprise_id existant + entreprise = Entreprise.query.filter_by( + siret=correspondant_data["siret_entreprise"], visible=True + ).first() + if entreprise is None: + return False + + # correspondant possède le meme nom et prénom dans la meme entreprise + correspondant = EntrepriseCorrespondant.query.filter_by( + nom=correspondant_data["nom"], + prenom=correspondant_data["prenom"], + entreprise_id=entreprise.id, + ).first() + if correspondant is not None: + return False + + return True + + +def list_to_dict(m): + l = [] + for data in m[1:]: + new_dict = {title: value.strip() for title, value in zip(m[0], data)} + l.append(new_dict) + + return l diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index a16190070..61d517b7c 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -26,6 +26,7 @@ import re import requests +from datetime import datetime from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed, FileRequired @@ -33,6 +34,7 @@ from markupsafe import Markup from sqlalchemy import text from wtforms import ( StringField, + IntegerField, SubmitField, TextAreaField, SelectField, @@ -42,14 +44,23 @@ from wtforms import ( BooleanField, FieldList, FormField, + BooleanField, +) +from wtforms.validators import ( + ValidationError, + DataRequired, + Email, + Optional, + NumberRange, ) -from wtforms.validators import ValidationError, DataRequired, Email, Optional from wtforms.widgets import ListWidget, CheckboxInput from app.entreprises.models import ( Entreprise, EntrepriseCorrespondant, EntreprisePreferences, + EntrepriseSite, + EntrepriseTaxeApprentissage, ) from app.models import Identite, Departement from app.auth.models import User @@ -69,11 +80,17 @@ def _build_string_field(label, required=True, render_kw=None): return StringField(label, validators=[Optional()], render_kw=render_kw) +class EntreprisesFilterForm(FlaskForm): + active = BooleanField("Toutes les entreprises") + association = BooleanField("Seulement les associations partenaires") + + class EntrepriseCreationForm(FlaskForm): siret = _build_string_field( "SIRET (*)", render_kw={"placeholder": "Numéro composé de 14 chiffres"}, ) + 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 (*)") @@ -153,8 +170,8 @@ class EntrepriseCreationForm(FlaskForm): class EntrepriseModificationForm(FlaskForm): - hidden_entreprise_siret = HiddenField() siret = StringField("SIRET (*)") + association = BooleanField("Association") nom = _build_string_field("Nom de l'entreprise (*)") adresse = _build_string_field("Adresse (*)") codepostal = _build_string_field("Code postal (*)") @@ -164,13 +181,11 @@ class EntrepriseModificationForm(FlaskForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.siret.render_kw = { - "disabled": "", - "value": self.hidden_entreprise_siret.data, - } + self.siret.render_kw = {"disabled": ""} 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 (*)") @@ -178,6 +193,49 @@ class SiteCreationForm(FlaskForm): 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) @@ -214,7 +272,7 @@ class OffreCreationForm(FlaskForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.correspondant.choices = [ + self.correspondant.choices = [("", "")] + [ (correspondant.id, f"{correspondant.nom} {correspondant.prenom}") for correspondant in EntrepriseCorrespondant.query.filter_by( entreprise_id=self.hidden_entreprise_id.data @@ -260,7 +318,7 @@ class OffreModificationForm(FlaskForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.correspondant.choices = [ + self.correspondant.choices = [("", "")] + [ (correspondant.id, f"{correspondant.nom} {correspondant.prenom}") for correspondant in EntrepriseCorrespondant.query.filter_by( entreprise_id=self.hidden_entreprise_id.data @@ -290,8 +348,16 @@ class CorrespondantCreationForm(FlaskForm): validators=[DataRequired(message=CHAMP_REQUIS)], render_kw={"class": "form-control"}, ) - nom = _build_string_field("Nom (*)", render_kw={"class": "form-control"}) - prenom = _build_string_field("Prénom (*)", 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"} ) @@ -321,7 +387,6 @@ class CorrespondantCreationForm(FlaskForm): 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 @@ -558,9 +623,75 @@ class StageApprentissageModificationForm(FlaskForm): 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( - _build_string_field( + StringField( "Responsable (*)", render_kw={ "placeholder": "Tapez le nom du responsable de formation", @@ -573,6 +704,8 @@ class EnvoiOffreForm(FlaskForm): def validate(self): validate = True + list_select = True + if not FlaskForm.validate(self): validate = False @@ -588,8 +721,12 @@ class EnvoiOffreForm(FlaskForm): .first() ) if responsable is None: - entry.errors.append("Champ incorrect (selectionnez dans la liste)") - validate = False + validate, list_select = False, False + + if list_select is False: + self.responsables.errors.append( + "Champ incorrect (selectionnez dans la liste)" + ) return validate @@ -611,7 +748,11 @@ class SuppressionConfirmationForm(FlaskForm): class DesactivationConfirmationForm(FlaskForm): notes_active = TextAreaField("Notes sur la désactivation", validators=[Optional()]) - submit = SubmitField("Désactiver", render_kw=SUBMIT_MARGE) + submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) + + +class ActivationConfirmationForm(FlaskForm): + submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) class ValidationConfirmationForm(FlaskForm): @@ -633,6 +774,7 @@ 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) diff --git a/app/entreprises/models.py b/app/entreprises/models.py index 4a318d24a..4edbe458b 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -10,6 +10,7 @@ class Entreprise(db.Model): codepostal = db.Column(db.Text) ville = db.Column(db.Text) pays = db.Column(db.Text) + association = db.Column(db.Boolean, default=False) visible = db.Column(db.Boolean, default=False) active = db.Column(db.Boolean, default=True) notes_active = db.Column(db.Text) @@ -36,6 +37,10 @@ class Entreprise(db.Model): "code_postal": self.codepostal, "ville": self.ville, "pays": self.pays, + "association": self.association, + "visible": self.visible, + "active": self.active, + "notes_active": self.notes_active, } @@ -58,6 +63,18 @@ class EntrepriseSite(db.Model): cascade="all, delete-orphan", ) + def to_dict(self): + entreprise = Entreprise.query.get_or_404(self.entreprise_id) + return { + "siret_entreprise": entreprise.siret, + "id_site": self.id, + "nom_site": self.nom, + "adresse": self.adresse, + "code_postal": self.codepostal, + "ville": self.ville, + "pays": self.pays, + } + class EntrepriseCorrespondant(db.Model): __tablename__ = "are_correspondants" @@ -77,8 +94,9 @@ class EntrepriseCorrespondant(db.Model): notes = db.Column(db.Text) def to_dict(self): - entreprise = Entreprise.query.filter_by(id=self.entreprise_id).first() + site = EntrepriseSite.query.get_or_404(self.site_id) return { + "id": self.id, "civilite": self.civilite, "nom": self.nom, "prenom": self.prenom, @@ -88,7 +106,7 @@ class EntrepriseCorrespondant(db.Model): "service": self.service, "origine": self.origine, "notes": self.notes, - "entreprise_siret": entreprise.siret, + "nom_site": site.nom, } @@ -131,12 +149,14 @@ class EntrepriseOffre(db.Model): } -class EntrepriseLog(db.Model): - __tablename__ = "are_logs" +class EntrepriseHistorique(db.Model): + __tablename__ = "are_historique" id = db.Column(db.Integer, primary_key=True) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) authenticated_user = db.Column(db.Text) - object = db.Column(db.Integer) + entreprise_id = db.Column(db.Integer) + object = db.Column(db.Text) + object_id = db.Column(db.Integer) text = db.Column(db.Text) @@ -155,6 +175,17 @@ class EntrepriseStageApprentissage(db.Model): notes = db.Column(db.Text) +class EntrepriseTaxeApprentissage(db.Model): + __tablename__ = "are_taxe_apprentissage" + id = db.Column(db.Integer, primary_key=True) + entreprise_id = db.Column( + db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade") + ) + annee = db.Column(db.Integer) + montant = db.Column(db.Integer) + notes = db.Column(db.Text) + + class EntrepriseEnvoiOffre(db.Model): __tablename__ = "are_envoi_offre" id = db.Column(db.Integer, primary_key=True) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index b815b2297..6a831d866 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -12,11 +12,14 @@ from app.decorators import permission_required from app.entreprises import LOGS_LEN from app.entreprises.forms import ( + ActivationConfirmationForm, CorrespondantsCreationForm, DesactivationConfirmationForm, EntrepriseCreationForm, EntrepriseModificationForm, + EntreprisesFilterForm, SiteCreationForm, + SiteModificationForm, SuppressionConfirmationForm, OffreCreationForm, OffreModificationForm, @@ -27,6 +30,8 @@ from app.entreprises.forms import ( StageApprentissageModificationForm, EnvoiOffreForm, AjoutFichierForm, + TaxeApprentissageForm, + TaxeApprentissageModificationForm, ValidationConfirmationForm, ImportForm, PreferencesForm, @@ -36,13 +41,14 @@ from app.entreprises.models import ( Entreprise, EntrepriseOffre, EntrepriseCorrespondant, - EntrepriseLog, + EntrepriseHistorique, EntrepriseContact, EntrepriseSite, EntrepriseStageApprentissage, EntrepriseEnvoiOffre, EntrepriseOffreDepartement, EntreprisePreferences, + EntrepriseTaxeApprentissage, ) from app.entreprises import app_relations_entreprises as are from app.models import Identite @@ -52,18 +58,40 @@ from app.scodoc import sco_etud, sco_excel import app.scodoc.sco_utils as scu from app import db -from sqlalchemy import text +from sqlalchemy import text, sql from werkzeug.utils import secure_filename -@bp.route("/", methods=["GET"]) +@bp.route("/", methods=["GET", "POST"]) @permission_required(Permission.RelationsEntreprisesView) def index(): """ Permet d'afficher une page avec la liste des entreprises (visible) et une liste des dernières opérations """ entreprises = Entreprise.query.filter_by(visible=True, active=True) - logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() + logs = ( + EntrepriseHistorique.query.order_by(EntrepriseHistorique.date.desc()) + .limit(LOGS_LEN) + .all() + ) + if current_user.has_permission(Permission.RelationsEntreprisesChange, None): + form = EntreprisesFilterForm() + checked = [False, False] + if request.method == "POST": + checked[0] = form.active.data + checked[1] = form.association.data + if checked[0]: + entreprises = Entreprise.query.filter_by(visible=True) + if checked[1]: + entreprises = Entreprise.query.filter_by(association=True) + return render_template( + "entreprises/entreprises.html", + title="Entreprises", + entreprises=entreprises, + logs=logs, + form=form, + checked=checked, + ) return render_template( "entreprises/entreprises.html", title="Entreprises", @@ -79,9 +107,9 @@ def logs(): Permet d'afficher les logs (toutes les entreprises) """ page = request.args.get("page", 1, type=int) - logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).paginate( - page=page, per_page=20 - ) + logs = EntrepriseHistorique.query.order_by( + EntrepriseHistorique.date.desc() + ).paginate(page=page, per_page=20) return render_template( "entreprises/logs.html", title="Logs", @@ -110,11 +138,17 @@ def correspondants(): Permet d'afficher une page avec la liste des correspondants des entreprises visibles et une liste des dernières opérations """ correspondants = ( - db.session.query(EntrepriseCorrespondant, Entreprise) - .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id) + db.session.query(EntrepriseCorrespondant, EntrepriseSite) + .join(EntrepriseSite, EntrepriseCorrespondant.site_id == EntrepriseSite.id) + .join(Entreprise, EntrepriseSite.entreprise_id == Entreprise.id) .filter_by(visible=True, active=True) + .all() + ) + logs = ( + EntrepriseHistorique.query.order_by(EntrepriseHistorique.date.desc()) + .limit(LOGS_LEN) + .all() ) - logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() return render_template( "entreprises/correspondants.html", title="Correspondants", @@ -132,9 +166,9 @@ def fiche_entreprise(id): La fiche entreprise comporte les informations de l'entreprise, les correspondants de l'entreprise et les offres de l'entreprise. """ - entreprise = Entreprise.query.filter_by( - id=id, visible=True, active=True - ).first_or_404(description=f"fiche entreprise {id} inconnue") + entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( + description=f"fiche entreprise {id} inconnue" + ) offres_with_files = [] depts = are.get_depts() for offre in entreprise.offres: @@ -150,8 +184,8 @@ def fiche_entreprise(id): offres_with_files.append(offre_with_files) sites = entreprise.sites[:] logs = ( - EntrepriseLog.query.order_by(EntrepriseLog.date.desc()) - .filter_by(object=id) + EntrepriseHistorique.query.order_by(EntrepriseHistorique.date.desc()) + .filter(EntrepriseHistorique.entreprise_id == id) .limit(LOGS_LEN) .all() ) @@ -162,6 +196,11 @@ def fiche_entreprise(id): .join(Identite, Identite.id == EntrepriseStageApprentissage.etudid) .all() ) + taxes = ( + EntrepriseTaxeApprentissage.query.filter_by(entreprise_id=id) + .order_by(EntrepriseTaxeApprentissage.annee.desc()) + .all() + ) return render_template( "entreprises/fiche_entreprise.html", title="Fiche entreprise", @@ -170,6 +209,7 @@ def fiche_entreprise(id): offres=offres_with_files, logs=logs, stages_apprentissages=stages_apprentissages, + taxes=taxes, ) @@ -184,8 +224,8 @@ def logs_entreprise(id): description=f"logs fiche entreprise {id} inconnu" ) logs = ( - EntrepriseLog.query.order_by(EntrepriseLog.date.desc()) - .filter_by(object=id) + EntrepriseHistorique.query.order_by(EntrepriseHistorique.date.desc()) + .filter(EntrepriseHistorique.entreprise_id == entreprise.id) .paginate(page=page, per_page=20) ) return render_template( @@ -205,12 +245,12 @@ def fiche_entreprise_validation(id): entreprise = Entreprise.query.filter_by(id=id, visible=False).first_or_404( description=f"fiche entreprise (validation) {id} inconnue" ) - correspondants = entreprise.correspondants + sites = entreprise.sites[:] return render_template( "entreprises/fiche_entreprise_validation.html", title="Validation fiche entreprise", entreprise=entreprise, - correspondants=correspondants, + sites=sites, ) @@ -290,6 +330,7 @@ def add_entreprise(): entreprise = Entreprise( nom=form.nom_entreprise.data.strip(), siret=form.siret.data.strip(), + association=form.association.data, adresse=form.adresse.data.strip(), codepostal=form.codepostal.data.strip(), ville=form.ville.data.strip(), @@ -327,9 +368,10 @@ def add_entreprise(): if current_user.has_permission(Permission.RelationsEntreprisesValidate, None): entreprise.visible = True nom_entreprise = f"{entreprise.nom}" - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, text=f"{nom_entreprise} - Création de la fiche entreprise ({entreprise.nom})", + entreprise_id=entreprise.id, ) db.session.add(log) db.session.commit() @@ -360,51 +402,52 @@ def edit_entreprise(id): entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( description=f"entreprise {id} inconnue" ) - form = EntrepriseModificationForm(hidden_entreprise_siret=entreprise.siret) + form = EntrepriseModificationForm(siret=entreprise.siret) if form.validate_on_submit(): nom_entreprise = f"{form.nom.data.strip()}" if entreprise.nom != form.nom.data.strip(): - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=entreprise.id, text=f"{nom_entreprise} - Modification du nom (ancien nom: {entreprise.nom})", ) entreprise.nom = form.nom.data.strip() db.session.add(log) if entreprise.adresse != form.adresse.data.strip(): - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=entreprise.id, text=f"{nom_entreprise} - Modification de l'adresse (ancienne adresse: {entreprise.adresse})", ) entreprise.adresse = form.adresse.data.strip() db.session.add(log) if entreprise.codepostal != form.codepostal.data.strip(): - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=entreprise.id, text=f"{nom_entreprise} - Modification du code postal (ancien code postal: {entreprise.codepostal})", ) entreprise.codepostal = form.codepostal.data.strip() db.session.add(log) if entreprise.ville != form.ville.data.strip(): - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=entreprise.id, text=f"{nom_entreprise} - Modification de la ville (ancienne ville: {entreprise.ville})", ) entreprise.ville = form.ville.data.strip() db.session.add(log) if entreprise.pays != form.pays.data.strip() or not form.pays.data.strip(): - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=entreprise.id, text=f"{nom_entreprise} - Modification du pays (ancien pays: {entreprise.pays})", ) entreprise.pays = ( form.pays.data.strip() if form.pays.data.strip() else "FRANCE" ) db.session.add(log) + entreprise.association = form.association.data db.session.commit() flash("L'entreprise a été modifié.") return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) @@ -415,6 +458,7 @@ def edit_entreprise(id): form.codepostal.data = entreprise.codepostal form.ville.data = entreprise.ville form.pays.data = entreprise.pays + form.association.data = entreprise.association return render_template( "entreprises/form_modification_entreprise.html", title="Modification entreprise", @@ -428,21 +472,128 @@ def fiche_entreprise_desactiver(id): """ Permet de désactiver une entreprise """ - entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( - description=f"entreprise {id} inconnue" - ) + entreprise = Entreprise.query.filter_by( + id=id, visible=True, active=True + ).first_or_404(description=f"entreprise {id} inconnue") form = DesactivationConfirmationForm() if form.validate_on_submit(): entreprise.notes_active = form.notes_active.data.strip() entreprise.active = False db.session.commit() flash("L'entreprise a été désactivé.") - return redirect(url_for("entreprises.index")) + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) return render_template( "entreprises/confirmation_form.html", title="Désactiver entreprise", form=form, - info_message="Cliquez sur le bouton Désactiver pour confirmer la désactivation", + info_message="Cliquez sur le bouton Modifier pour confirmer la désactivation", + ) + + +@bp.route("/fiche_entreprise/activer/", methods=["GET", "POST"]) +@permission_required(Permission.RelationsEntreprisesChange) +def fiche_entreprise_activer(id): + """ + Permet d'activer une entreprise + """ + entreprise = Entreprise.query.filter_by( + id=id, visible=True, active=False + ).first_or_404(description=f"entreprise {id} inconnue") + form = ActivationConfirmationForm() + if form.validate_on_submit(): + entreprise.active = True + db.session.commit() + flash("L'entreprise a été activé.") + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + return render_template( + "entreprises/confirmation_form.html", + title="Activer entreprise", + form=form, + info_message="Cliquez sur le bouton Modifier pour confirmer l'activaction", + ) + + +@bp.route("/fiche_entreprise//add_taxe_apprentissage", methods=["GET", "POST"]) +def add_taxe_apprentissage(id): + """ + Permet d'ajouter une taxe d'apprentissage sur un fiche entreprise + """ + entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( + description=f"entreprise {id} inconnue" + ) + form = TaxeApprentissageForm(hidden_entreprise_id=id) + if form.validate_on_submit(): + taxe = EntrepriseTaxeApprentissage( + entreprise_id=entreprise.id, + annee=form.annee.data, + montant=form.montant.data, + notes=form.notes.data.strip(), + ) + db.session.add(taxe) + db.session.commit() + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + return render_template( + "entreprises/form.html", + title="Ajout taxe apprentissage", + form=form, + ) + + +@bp.route( + "/fiche_entreprise//edit_taxe_apprentissage/", + methods=["GET", "POST"], +) +def edit_taxe_apprentissage(id_entreprise, id_taxe): + """ + Permet de modifier une taxe d'apprentissage sur un fiche entreprise + """ + entreprise = Entreprise.query.filter_by( + id=id_entreprise, visible=True + ).first_or_404(description=f"entreprise {id_entreprise} inconnue") + taxe = EntrepriseTaxeApprentissage.query.filter_by(id=id_taxe).first_or_404( + description=f"taxe d'apprentissage {id_taxe} inconnue" + ) + form = TaxeApprentissageModificationForm(annee=taxe.annee) + if form.validate_on_submit(): + taxe.montant = form.montant.data + taxe.notes = form.notes.data.strip() + db.session.commit() + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + elif request.method == "GET": + form.montant.data = taxe.montant + form.notes.data = taxe.notes + return render_template( + "entreprises/form.html", + title="Modification taxe apprentissage", + form=form, + ) + + +@bp.route( + "/fiche_entreprise//delete_taxe_apprentissage/", + methods=["GET", "POST"], +) +def delete_taxe_apprentissage(id_entreprise, id_taxe): + """ + Permet de modifier une taxe d'apprentissage sur un fiche entreprise + """ + entreprise = Entreprise.query.filter_by( + id=id_entreprise, visible=True + ).first_or_404(description=f"entreprise {id_entreprise} inconnue") + taxe = EntrepriseTaxeApprentissage.query.filter_by(id=id_taxe).first_or_404( + description=f"taxe d'apprentissage {id_taxe} inconnue" + ) + form = SuppressionConfirmationForm() + if form.validate_on_submit(): + db.session.delete(taxe) + db.session.commit() + flash("La taxe d'apprentissage a été supprimé de la liste.") + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + return render_template( + "entreprises/confirmation_form.html", + title="Supprimer taxe apprentissage", + form=form, + info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression", ) @@ -461,9 +612,10 @@ def validate_entreprise(id): if form.validate_on_submit(): entreprise.visible = True nom_entreprise = f"{entreprise.nom}" - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - text=f"{nom_entreprise} - Validation de la fiche entreprise ({entreprise.nom}) avec un correspondant", + entreprise_id=entreprise.id, + text=f"{nom_entreprise} - Validation de la fiche entreprise ({entreprise.nom})", ) db.session.add(log) db.session.commit() @@ -521,7 +673,9 @@ def add_offre(id): missions=form.missions.data.strip(), duree=form.duree.data.strip(), expiration_date=form.expiration_date.data, - correspondant_id=form.correspondant.data, + correspondant_id=form.correspondant.data + if form.correspondant.data != "" + else None, ) db.session.add(offre) db.session.commit() @@ -545,9 +699,11 @@ def add_offre(id): file = form.fichier.data filename = secure_filename(file.filename) file.save(os.path.join(path, filename)) - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=entreprise.id, + object="offre", + object_id=offre.id, text="Création d'une offre", ) db.session.add(log) @@ -582,7 +738,10 @@ def edit_offre(id): offre.missions = form.missions.data.strip() offre.duree = form.duree.data.strip() offre.expiration_date = form.expiration_date.data - offre.correspondant_id = form.correspondant.data + if form.correspondant.data == "": + offre.correspondant_id = sql.null() + else: + offre.correspondant_id = form.correspondant.data if offre_depts_list != form.depts.data: for dept in form.depts.data: if dept not in offre_depts_list: @@ -597,9 +756,11 @@ def edit_offre(id): offre_id=offre.id, dept_id=dept ).first_or_404() db.session.delete(offre_dept) - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=offre.entreprise_id, + entreprise_id=offre.entreprise_id, + object="offre", + object_id=offre.id, text="Modification d'une offre", ) db.session.add(log) @@ -642,9 +803,11 @@ def delete_offre(id): ) if os.path.isdir(path): shutil.rmtree(path) - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=offre.entreprise_id, + entreprise_id=offre.entreprise_id, + object="offre", + object_id=offre.id, text="Suppression d'une offre", ) db.session.add(log) @@ -700,7 +863,7 @@ def add_site(id): entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( description=f"entreprise {id} inconnue" ) - form = SiteCreationForm() + form = SiteCreationForm(hidden_entreprise_id=id) if form.validate_on_submit(): site = EntrepriseSite( entreprise_id=entreprise.id, @@ -721,6 +884,41 @@ def add_site(id): ) +@bp.route( + "/fiche_entreprise//edit_site/", + methods=["GET", "POST"], +) +def edit_site(id_entreprise, id_site): + entreprise = Entreprise.query.filter_by( + id=id_entreprise, visible=True + ).first_or_404(description=f"entreprise {id_entreprise} inconnue") + site = EntrepriseSite.query.filter_by( + id=id_site, entreprise_id=entreprise.id + ).first_or_404(description=f"site {id_site} inconnu") + form = SiteModificationForm( + hidden_entreprise_id=id_entreprise, hidden_site_id=id_site + ) + if form.validate_on_submit(): + site.nom = form.nom.data.strip() + site.adresse = form.adresse.data.strip() + site.codepostal = form.codepostal.data.strip() + site.ville = form.ville.data.strip() + site.pays = (form.pays.data.strip() if form.pays.data.strip() else "FRANCE",) + db.session.commit() + return redirect(url_for("entreprises.fiche_entreprise", id=site.entreprise_id)) + elif request.method == "GET": + form.nom.data = site.nom + form.adresse.data = site.adresse + form.codepostal.data = site.codepostal + form.ville.data = site.ville + form.pays.data = site.pays + return render_template( + "entreprises/form.html", + title="Modification site", + form=form, + ) + + @bp.route( "/fiche_entreprise//add_correspondant/", methods=["GET", "POST"], @@ -752,13 +950,17 @@ def add_correspondant(id_entreprise, id_site): origine=correspondant_entry.origine.data.strip(), notes=correspondant_entry.notes.data.strip(), ) - log = EntrepriseLog( + db.session.add(correspondant) + db.session.commit() + db.session.refresh(correspondant) + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=entreprise.id, + entreprise_id=correspondant.entreprise_id, + object="correspondant", + object_id=correspondant.id, text="Création d'un correspondant", ) db.session.add(log) - db.session.add(correspondant) db.session.commit() flash("Le correspondant a été ajouté à la fiche entreprise.") return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) @@ -775,8 +977,11 @@ def edit_correspondant(id): """ Permet de modifier un correspondant """ - correspondant = EntrepriseCorrespondant.query.filter_by(id=id).first_or_404( - description=f"correspondant {id} inconnu" + correspondant = ( + db.session.query(EntrepriseCorrespondant) + .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id) + .filter(EntrepriseCorrespondant.id == id, Entreprise.visible == True) + .first_or_404(description=f"correspondant {id} inconnu") ) form = CorrespondantModificationForm( hidden_entreprise_id=correspondant.entreprise_id, @@ -792,9 +997,11 @@ def edit_correspondant(id): correspondant.service = form.service.data.strip() correspondant.origine = form.origine.data.strip() correspondant.notes = form.notes.data.strip() - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=correspondant.entreprise_id, + entreprise_id=correspondant.entreprise_id, + object="correspondant", + object_id=correspondant.id, text="Modification d'un correspondant", ) db.session.add(log) @@ -826,15 +1033,20 @@ def delete_correspondant(id): """ Permet de supprimer un correspondant """ - correspondant = EntrepriseCorrespondant.query.filter_by(id=id).first_or_404( - description=f"correspondant {id} inconnu" + correspondant = ( + db.session.query(EntrepriseCorrespondant) + .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id) + .filter(EntrepriseCorrespondant.id == id, Entreprise.visible == True) + .first_or_404(description=f"correspondant {id} inconnu") ) form = SuppressionConfirmationForm() if form.validate_on_submit(): db.session.delete(correspondant) - log = EntrepriseLog( + log = EntrepriseHistorique( authenticated_user=current_user.user_name, - object=correspondant.entreprise_id, + entreprise_id=correspondant.entreprise_id, + object="correspondant", + object_id=correspondant.id, text="Suppression d'un correspondant", ) db.session.add(log) @@ -937,7 +1149,10 @@ def contacts(id): """ Permet d'afficher une page avec la liste des contacts d'une entreprise """ - contacts = EntrepriseContact.query.filter_by(entreprise=id).all() + entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( + description=f"entreprise {id} inconnue" + ) + contacts = EntrepriseContact.query.filter_by(entreprise=entreprise.id).all() return render_template( "entreprises/contacts.html", title="Liste des contacts", @@ -1125,7 +1340,7 @@ def json_etudiants(): if request.args.get("term") is None: abort(400) term = request.args.get("term").strip() - etudiants = Identite.query.filter(Identite.nom.ilike(f"%{term}%")).all() + etudiants = Identite.query.filter(Identite.nom.ilike(f"%{term}%")).limit(30).all() list = [] for etudiant in etudiants: content = {} @@ -1134,10 +1349,14 @@ def json_etudiants(): content = { "id": f"{etudiant.id}", "value": value, - "info": f"{etudiant.inscription_courante().formsemestre.titre}", + "info": f"Département {are.get_dept_acronym_by_id(etudiant.dept_id)} - {etudiant.inscription_courante().formsemestre.titre}", } else: - content = {"id": f"{etudiant.id}", "value": value} + content = { + "id": f"{etudiant.id}", + "value": value, + "info": f"Département {are.get_dept_acronym_by_id(etudiant.dept_id)}", + } list.append(content) return jsonify(results=list) @@ -1151,68 +1370,56 @@ def json_responsables(): if request.args.get("term") is None: abort(400) term = request.args.get("term").strip() - responsables = User.query.filter( - User.nom.ilike(f"%{term}%"), User.nom.is_not(None), User.prenom.is_not(None) - ).all() + responsables = ( + User.query.filter( + User.nom.ilike(f"%{term}%"), User.nom.is_not(None), User.prenom.is_not(None) + ) + .limit(30) + .all() + ) list = [] for responsable in responsables: content = {} value = f"{responsable.get_nomplogin()}" - content = {"id": f"{responsable.id}", "value": value, "info": ""} + content = {"id": f"{responsable.id}", "value": value} list.append(content) return jsonify(results=list) -@bp.route("/export_entreprises") +@bp.route("/export_donnees") @permission_required(Permission.RelationsEntreprisesExport) -def export_entreprises(): +def export_donnees(): """ Permet d'exporter la liste des entreprises sous format excel (.xlsx) """ - entreprises = Entreprise.query.filter_by(visible=True).all() - if entreprises: - keys = ["siret", "nom_entreprise", "adresse", "ville", "code_postal", "pays"] - titles = keys[:] - L = [ - [entreprise.to_dict().get(k, "") for k in keys] - for entreprise in entreprises - ] - title = "Entreprises" - xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) - filename = title + entreprise = Entreprise.query.filter_by(visible=True).first() + if entreprise: + wb = are.get_excel_book_are(export=True) + xlsx = wb.generate() + filename = "ExportApplicationRelationsEntreprises" return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) else: - flash("Aucune entreprise dans la base.") return redirect(url_for("entreprises.index")) -@bp.route("/import_entreprises/get_import_entreprises_file_sample") +@bp.route("/import_donnees/get_file_sample") @permission_required(Permission.RelationsEntreprisesExport) -def get_import_entreprises_file_sample(): +def get_import_donnees_file_sample(): """ Permet de récupérer un fichier exemple vide pour pouvoir importer des entreprises """ - keys = [ - "siret", - "nom_entreprise", - "adresse", - "ville", - "code_postal", - "pays", - ] - titles = keys[:] - title = "ImportEntreprises" - xlsx = sco_excel.excel_simple_table(titles=titles, sheet_name="Entreprises") - filename = title + wb = are.get_excel_book_are() + xlsx = wb.generate() + filename = "ImportApplicationRelationsEntreprises" return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) -@bp.route("/import_entreprises", methods=["GET", "POST"]) +@bp.route("/import_donnees", methods=["GET", "POST"]) @permission_required(Permission.RelationsEntreprisesExport) -def import_entreprises(): +def import_donnees(): """ - Permet d'importer des entreprises a l'aide d'un fichier excel (.xlsx) + Permet d'importer des entreprises à partir d'un fichier excel (.xlsx) """ form = ImportForm() if form.validate_on_submit(): @@ -1221,237 +1428,58 @@ def import_entreprises(): Config.SCODOC_VAR_DIR, "tmp", secure_filename(file.filename) ) file.save(file_path) - data = sco_excel.excel_file_to_list(file_path) + diag, lm = sco_excel.excel_workbook_to_list(file_path) os.remove(file_path) - entreprises_import = [] - siret_list = [] - ligne = 0 - titles = ["siret", "nom_entreprise", "adresse", "ville", "code_postal", "pays"] - if data[1][0] != titles: + if lm is None or len(lm) < 2: flash("Veuillez utilisez la feuille excel à remplir") - return render_template( - "entreprises/import_entreprises.html", - title="Importation entreprises", - form=form, - ) - for entreprise_data in data[1][1:]: - ligne += 1 - if ( - are.verif_entreprise_data(entreprise_data) - and entreprise_data[0].replace(" ", "") not in siret_list - ): - siret_list.append(entreprise_data[0].replace(" ", "")) - entreprise = Entreprise( - siret=entreprise_data[0].replace(" ", ""), - nom=entreprise_data[1].strip(), - adresse=entreprise_data[2].strip(), - ville=entreprise_data[3].strip(), - codepostal=entreprise_data[4].strip(), - pays=entreprise_data[5].strip(), - visible=True, - ) - entreprises_import.append(entreprise) - else: - flash(f"Erreur lors de l'importation (ligne {ligne})") - return render_template( - "entreprises/import_entreprises.html", - title="Importation entreprises", - form=form, - ) - - if len(entreprises_import) > 0: - for entreprise in entreprises_import: - db.session.add(entreprise) - log = EntrepriseLog( - authenticated_user=current_user.user_name, - text=f"Importation de {len(entreprises_import)} entreprise(s)", - ) - db.session.add(log) + return redirect(url_for("entreprises.import_donnees")) + entreprises_import = are.check_entreprises_import(lm[0]) + sites_import, correspondants_import = are.check_sites_import(lm[1]) + if ( + entreprises_import is False + or sites_import is False + or correspondants_import is False + or (len(lm) > 2 and are.check_correspondants_import(lm[2]) is False) + ): + return redirect(url_for("entreprises.import_donnees")) + for entreprise in entreprises_import: + db.session.add(entreprise) db.session.commit() - flash(f"Importation réussie de {len(entreprises_import)} entreprise(s)") - return render_template( - "entreprises/import_entreprises.html", - title="Importation entreprises", - form=form, - entreprises_import=entreprises_import, + db.session.refresh(entreprise) + site = EntrepriseSite( + entreprise_id=entreprise.id, + nom=entreprise.nom, + adresse=entreprise.adresse, + codepostal=entreprise.codepostal, + ville=entreprise.ville, + pays=entreprise.pays, ) - else: - flash('Feuille "Entreprises" vide') - - return render_template( - "entreprises/import_entreprises.html", - title="Importation entreprises", - form=form, - ) - - -@bp.route("/export_correspondants") -@permission_required(Permission.RelationsEntreprisesExport) -def export_correspondants(): - """ - Permet d'exporter la liste des correspondants sous format excel (.xlsx) - """ - correspondants = ( - db.session.query(EntrepriseCorrespondant) - .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id) - .filter_by(visible=True) - .all() - ) - if correspondants: - keys = [ - "civilite", - "nom", - "prenom", - "telephone", - "mail", - "poste", - "service", - "origine", - "notes", - "entreprise_siret", - ] - titles = keys[:] - L = [ - [correspondant.to_dict().get(k, "") for k in keys] - for correspondant in correspondants - ] - title = "Correspondants" - xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) - filename = title - return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) - else: - flash("Aucun correspondant dans la base.") - return redirect(url_for("entreprises.correspondants")) - - -@bp.route("/import_correspondants/get_import_correspondants_file_sample") -@permission_required(Permission.RelationsEntreprisesExport) -def get_import_correspondants_file_sample(): - """ - Permet de récupérer un fichier exemple vide pour pouvoir importer des correspondants - """ - keys = [ - "civilite", - "nom", - "prenom", - "telephone", - "mail", - "poste", - "service", - "origine", - "notes", - "entreprise_siret", - ] - titles = keys[:] - title = "ImportCorrespondants" - xlsx = sco_excel.excel_simple_table(titles=titles, sheet_name="Correspondants") - filename = title - return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) - - -@bp.route("/import_correspondants", methods=["GET", "POST"]) -@permission_required(Permission.RelationsEntreprisesExport) -def import_correspondants(): - """ - Permet d'importer des correspondants a l'aide d'un fichier excel (.xlsx) - """ - form = ImportForm() - if form.validate_on_submit(): - file = form.fichier.data - file_path = os.path.join( - Config.SCODOC_VAR_DIR, "tmp", secure_filename(file.filename) - ) - file.save(file_path) - data = sco_excel.excel_file_to_list(file_path) - os.remove(file_path) - correspondants_import = [] - correspondant_list = [] - ligne = 0 - titles = [ - "civilite", - "nom", - "prenom", - "telephone", - "mail", - "poste", - "service", - "origine", - "notes", - "entreprise_siret", - ] - if data[1][0] != titles: - flash("Veuillez utilisez la feuille excel à remplir") - return render_template( - "entreprises/import_correspondants.html", - title="Importation correspondants", - form=form, - ) - for correspondant_data in data[1][1:]: - ligne += 1 - if ( - are.verif_correspondant_data(correspondant_data) - and ( - correspondant_data[1].strip(), - correspondant_data[2].strip(), - correspondant_data[9].strip(), - ) - not in correspondant_list - ): - correspondant_list.append( - ( - correspondant_data[1].strip(), - correspondant_data[2].strip(), - correspondant_data[9].strip(), - ) - ) - entreprise = Entreprise.query.filter_by( - siret=correspondant_data[9].strip() - ).first() - correspondant = EntrepriseCorrespondant( - civilite=correspondant_data[0].strip(), - nom=correspondant_data[1].strip(), - prenom=correspondant_data[2].strip(), - telephone=correspondant_data[3].strip(), - mail=correspondant_data[4].strip(), - poste=correspondant_data[5].strip(), - service=correspondant_data[6].strip(), - origine=correspondant_data[7].strip(), - notes=correspondant_data[8].strip(), - entreprise_id=entreprise.id, - ) - correspondants_import.append(correspondant) - else: - flash(f"Erreur lors de l'importation (ligne {ligne})") - return render_template( - "entreprises/import_correspondants.html", - title="Importation correspondants", - form=form, - ) - - if len(correspondants_import) > 0: - for correspondant in correspondants_import: + db.session.add(site) + for site in sites_import: + db.session.add(site) + correspondants = [] + for site, correspondant in correspondants_import: + if site is None: db.session.add(correspondant) - log = EntrepriseLog( - authenticated_user=current_user.user_name, - text=f"Importation de {len(correspondants_import)} correspondant(s)", - ) - db.session.add(log) - db.session.commit() - flash( - f"Importation réussie de {len(correspondants_import)} correspondant(s)" - ) - return render_template( - "entreprises/import_correspondants.html", - title="Importation correspondants", - form=form, - correspondants_import=correspondants_import, - ) - else: - flash('Feuille "Correspondants" vide') + else: + db.session.add(site) + db.session.commit() + db.session.refresh(site) + correspondant.site_id = site.id + db.session.add(correspondant) + correspondants.append(correspondant) + db.session.commit() + flash(f"Importation réussie") + return render_template( + "entreprises/import_donnees.html", + title="Importation données", + form=form, + entreprises_import=entreprises_import, + sites_import=sites_import, + correspondants_import=correspondants, + ) return render_template( - "entreprises/import_correspondants.html", - title="Importation correspondants", - form=form, + "entreprises/import_donnees.html", title="Importation données", form=form ) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index d6247207c..2c95c7ef4 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -40,10 +40,9 @@ from openpyxl.comments import Comment from openpyxl import Workbook, load_workbook from openpyxl.cell import WriteOnlyCell from openpyxl.styles import Font, Border, Side, Alignment, PatternFill +from openpyxl.worksheet.worksheet import Worksheet import app.scodoc.sco_utils as scu -from app.scodoc import notesdb -from app.scodoc import sco_preferences from app import log from app.scodoc.sco_exceptions import ScoValueError @@ -593,60 +592,87 @@ def excel_feuille_saisie(e, titreannee, description, lines): def excel_bytes_to_list(bytes_content): try: filelike = io.BytesIO(bytes_content) - return _excel_to_list(filelike) - except: + except Exception as exc: raise ScoValueError( """Le fichier xlsx attendu n'est pas lisible ! Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..) """ - ) + ) from exc + return _excel_to_list(filelike) def excel_file_to_list(filename): try: return _excel_to_list(filename) - except: + except Exception as exc: raise ScoValueError( """Le fichier xlsx attendu n'est pas lisible ! Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) """ - ) + ) from exc + + +def excel_workbook_to_list(filename): + try: + return _excel_workbook_to_list(filename) + except Exception as exc: + raise ScoValueError( + """Le fichier xlsx attendu n'est pas lisible ! + Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) + """ + ) from exc + + +def _open_workbook(filelike, dump_debug=False) -> Workbook: + """Open document. + On error, if dump-debug is True, dump data in /tmp for debugging purpose + """ + try: + workbook = load_workbook(filename=filelike, read_only=True, data_only=True) + except Exception as exc: + log("Excel_to_list: failure to import document") + if dump_debug: + dump_filename = "/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX + log(f"Dumping problemetic file on {dump_filename}") + with open(dump_filename, "wb") as f: + f.write(filelike) + raise ScoValueError( + "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel xlsx !" + ) from exc + return workbook def _excel_to_list(filelike): - """returns list of list - convert_to_string is a conversion function applied to all non-string values (ie numbers) - """ - try: - wb = load_workbook(filename=filelike, read_only=True, data_only=True) - except: - log("Excel_to_list: failure to import document") - with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f: - f.write(filelike) - raise ScoValueError( - "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" - ) + """returns list of list""" + workbook = _open_workbook(filelike) diag = [] # liste de chaines pour former message d'erreur - # n'utilise que la première feuille - if len(wb.get_sheet_names()) < 1: + if len(workbook.get_sheet_names()) < 1: diag.append("Aucune feuille trouvée dans le classeur !") return diag, None - if len(wb.get_sheet_names()) > 1: + # n'utilise que la première feuille: + if len(workbook.get_sheet_names()) > 1: diag.append("Attention: n'utilise que la première feuille du classeur !") + sheet_name = workbook.get_sheet_names()[0] + ws = workbook[sheet_name] + diag_sheet, matrix = _excel_sheet_to_list(ws, sheet_name) + diag += diag_sheet + return diag, matrix + + +def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]: + """read a spreadsheet sheet, and returns: + - diag : a list of strings (error messages aimed at helping the user) + - a list of lists: the spreadsheet cells + """ + diag = [] # fill matrix - sheet_name = wb.get_sheet_names()[0] - ws = wb.get_sheet_by_name(sheet_name) - sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") values = {} - for row in ws.iter_rows(): + for row in sheet.iter_rows(): for cell in row: if cell.value is not None: values[(cell.row - 1, cell.column - 1)] = str(cell.value) if not values: - diag.append( - "Aucune valeur trouvée dans la feuille %s !" - % sheet_name.decode(scu.SCO_ENCODING) - ) + diag.append(f"Aucune valeur trouvée dans la feuille {sheet_name} !") return diag, None indexes = list(values.keys()) # search numbers of rows and cols @@ -654,23 +680,38 @@ def _excel_to_list(filelike): cols = [x[1] for x in indexes] nbcols = max(cols) + 1 nbrows = max(rows) + 1 - m = [] + matrix = [] for _ in range(nbrows): - m.append([""] * nbcols) + matrix.append([""] * nbcols) for row_idx, col_idx in indexes: v = values[(row_idx, col_idx)] - # if isinstance(v, six.text_type): - # v = v.encode(scu.SCO_ENCODING, "backslashreplace") - # elif convert_to_string: - # v = convert_to_string(v) - m[row_idx][col_idx] = v - diag.append( - 'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m)) - ) - # diag.append(str(M)) - # - return diag, m + matrix[row_idx][col_idx] = v + diag.append(f'Feuille "{sheet_name}", {len(matrix)} lignes') + + return diag, matrix + + +def _excel_workbook_to_list(filelike): + """Lit un classeur (workbook): chaque feuille est lue + et est convertie en une liste de listes. + Returns: + - diag : a list of strings (error messages aimed at helping the user) + - a list of lists: the spreadsheet cells + """ + workbook = _open_workbook(filelike) + diag = [] # liste de chaines pour former message d'erreur + if len(workbook.get_sheet_names()) < 1: + diag.append("Aucune feuille trouvée dans le classeur !") + return diag, None + matrix_list = [] + for sheet_name in workbook.get_sheet_names(): + # fill matrix + sheet = workbook.get_sheet_by_name(sheet_name) + diag_sheet, matrix = _excel_sheet_to_list(sheet, sheet_name) + diag += diag_sheet + matrix_list.append(matrix) + return diag, matrix_list def excel_feuille_listeappel( diff --git a/app/static/css/entreprises.css b/app/static/css/entreprises.css index 0ede866cb..ce3a4ccda 100644 --- a/app/static/css/entreprises.css +++ b/app/static/css/entreprises.css @@ -1,28 +1,37 @@ -.nav-entreprise>ul { - padding-left: 0; +.nav-entreprise { + text-align: left; +} + +.nav-entreprise ul { + padding: 0; } .nav-entreprise li{ list-style: none; display: inline-block; padding: 10px; + border: 2px black solid; + border-radius: 10px; +} + +.nav-entreprise li:hover{ + background-color: rgb(212, 212, 212); } .nav-entreprise>ul>li>a { text-decoration: none; color: black; padding: 15px; - +} + +.nav-entreprise>ul>li>a:hover { + text-decoration: underline; } .form-error { color: #a94442; } -.nav-entreprise>ul>li>a:hover { - color: red; -} - .boutons .btn { margin-top: 5px; margin-bottom: 5px; @@ -59,14 +68,33 @@ margin-bottom: -5px; } -.entreprise, .correspondant, .offre, .site{ - border: solid 2px; +.entreprise, .correspondant, .offre, .site, .info-active { + border: solid 2px black; border-radius: 10px; padding: 10px; margin-bottom: 10px; margin-top: 10px; } +.info-active { + border-color: red; + background-color: rgb(250, 220, 220); +} + +.entreprise { + display: flex; + justify-content: space-between; +} + +.entreprise > div { + flex: 1 0 0; +} + +.taxe-apprentissage{ + overflow-y: scroll; + height: 100px; +} + .sites-et-offres { display: flex; justify-content: space-between; @@ -101,4 +129,47 @@ border: solid 2px; border-radius: 10px; padding: 10px; +} + +#liste-taxes-apprentissages { + list-style: none; + padding-left: 0; +} + +#form-entreprise-filter > label { + margin-right: 20px; +} + +.title-form-error { + font-weight: bold; + color: #a94442; +} + +.breadcrumbs { + padding: 0; +} + +.breadcrumbs_item { + display: inline-block; +} + +.breadcrumbs_item:not(:last-of-type)::after { + content: '\203a'; + margin: 0 5px; + color: #777; +} + +.breadcrumbs_link { + text-decoration: none; + color: #777; +} + +.breadcrumbs_link:hover { + text-decoration: underline; + color: #333; +} + +.breadcrumbs_link-active { + color: #333; + font-weight: bold; } \ No newline at end of file diff --git a/app/templates/entreprises/ajout_correspondants.html b/app/templates/entreprises/ajout_correspondants.html index 18070b7fc..fd0fe64b0 100644 --- a/app/templates/entreprises/ajout_correspondants.html +++ b/app/templates/entreprises/ajout_correspondants.html @@ -16,7 +16,11 @@

{{ form.hidden_tag() }} + {{ form.correspondants.label }} {% for subfield in form.correspondants %} + {% if subfield.errors %} +

Formulaire {{ subfield.label.text }}

+ {% endif %} {% for subsubfield in subfield %} {% if subsubfield.errors %} {% for error in subsubfield.errors %} @@ -56,7 +60,22 @@ } let newFieldName = `correspondants-${Math.max(...correspondantInputIds) + 1}`; allCorrepondantsFieldWrapper.insertAdjacentHTML('beforeend',` -
  • Retirer ce correspondant
  • +
  • + + + + + + + + + + + +
    + +
    Retirer ce correspondant
    +
  • `); }); } diff --git a/app/templates/entreprises/confirmation_form.html b/app/templates/entreprises/confirmation_form.html index fb8737887..5de63ba06 100644 --- a/app/templates/entreprises/confirmation_form.html +++ b/app/templates/entreprises/confirmation_form.html @@ -5,7 +5,7 @@ {% block app_content %}

    {{ title }}


    -
    {{ info_message }}
    +
    {{ info_message }}

    diff --git a/app/templates/entreprises/contacts.html b/app/templates/entreprises/contacts.html index 2896627e5..70ad459e0 100644 --- a/app/templates/entreprises/contacts.html +++ b/app/templates/entreprises/contacts.html @@ -9,7 +9,21 @@ {% endblock %} {% block app_content %} -
    +
    + +
    + +

    Liste des contacts

    {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} Ajouter contact diff --git a/app/templates/entreprises/correspondants.html b/app/templates/entreprises/correspondants.html index 047b57aa6..d389884a1 100644 --- a/app/templates/entreprises/correspondants.html +++ b/app/templates/entreprises/correspondants.html @@ -22,15 +22,6 @@
    {% endif %} -
    - {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) %} - Importer des correspondants - {% endif %} - {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) and correspondants %} - Exporter la liste des correspondants - {% endif %} -
    -

    Liste des correspondants

    @@ -54,7 +45,7 @@ - + {% endfor %} diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html index f6d08cec4..e58898bd3 100644 --- a/app/templates/entreprises/entreprises.html +++ b/app/templates/entreprises/entreprises.html @@ -27,15 +27,22 @@ Ajouter une entreprise {% endif %} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) %} - Importer des entreprises + Importer des données {% endif %} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) and entreprises %} - Exporter la liste des entreprises + Exporter des données {% endif %}

    Liste des entreprises

    + {% if form %} + + {{ form.hidden_tag() }} + {{ form.active.label }} + {{ form.association.label }} + + {% endif %}
    {{ correspondant[0].mail }} {{ correspondant[0].poste}} {{ correspondant[0].service}}{{ correspondant[1].nom }}{{ correspondant[1].nom }}
    @@ -53,7 +60,7 @@ {% for entreprise in entreprises %} - + @@ -67,7 +74,11 @@ diff --git a/app/templates/entreprises/envoi_offre_form.html b/app/templates/entreprises/envoi_offre_form.html index 6ead6477d..e8f2d55e1 100644 --- a/app/templates/entreprises/envoi_offre_form.html +++ b/app/templates/entreprises/envoi_offre_form.html @@ -18,13 +18,9 @@

    {{ form.hidden_tag() }} - {{ form.responsables.label }}
    - {% for subfield in form.responsables %} - {% if subfield.errors %} - {% for error in subfield.errors %} -

    {{ error }}

    - {% endfor %} - {% endif %} + {{ form.responsables.label }} + {% for error in form.responsables.errors %} +

    {{ error }}

    {% endfor %} {{ form.responsables }}
    @@ -71,7 +67,11 @@ } let newFieldName = `responsables-${Math.max(...responsableInputIds) + 1}`; allResponsablesFieldWrapper.insertAdjacentHTML('beforeend',` -
  • Retirer
  • +
  • + + +
    Retirer
    +
  • `); var as_r = new bsn.AutoSuggest(newFieldName, responsables_options); }); diff --git a/app/templates/entreprises/fiche_entreprise.html b/app/templates/entreprises/fiche_entreprise.html index 327a3bf7c..0b132cf12 100644 --- a/app/templates/entreprises/fiche_entreprise.html +++ b/app/templates/entreprises/fiche_entreprise.html @@ -9,6 +9,17 @@ {% endblock %} {% block app_content %} +
    + +
    + {% if logs %}

    Dernières opérations sur cette fiche Voir tout

    @@ -26,6 +37,15 @@

    Fiche entreprise - {{ entreprise.nom }} ({{ entreprise.siret }})

    + {% if not entreprise.active %} +
    + La fiche entreprise est désactivée
    + {% if entreprise.notes_active != "" %} + Notes : {{ entreprise.notes_active }} + {% endif %} +
    + {% endif %} +
    SIRET : {{ entreprise.siret }}
    @@ -33,14 +53,41 @@ Adresse : {{ entreprise.adresse }}
    Code postal : {{ entreprise.codepostal }}
    Ville : {{ entreprise.ville }}
    - Pays : {{ entreprise.pays }} + Pays : {{ entreprise.pays }}
    + {% if entreprise.association %} + Association + {% endif %}
    + + {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} +
    + Taxe d'apprentissage
    + Ajouter taxe apprentissage +
    +
      + {% if not taxes|check_taxe_now %} +
    • année actuelle : non versée
    • + {% endif %} + {% for taxe in taxes %} +
    • + supprimer + {{ taxe.annee }} : {{ taxe.montant }} euros {% if taxe.notes %}- {{ taxe.notes}} {% endif %} +
    • + {% endfor %} +
    +
    +
    + {% endif %}
    {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} Modifier + {% if entreprise.active %} Désactiver + {% else %} + Activer + {% endif %} Ajouter site Ajouter offre {% endif %} @@ -61,7 +108,10 @@ Ville : {{ site.ville }}
    Pays : {{ site.pays }} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} -
    Ajouter correspondant + {% endif %} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesCorrespondants, None) %} {% for correspondant in site.correspondants %} @@ -111,7 +161,7 @@
    - + {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} diff --git a/app/templates/entreprises/fiche_entreprise_validation.html b/app/templates/entreprises/fiche_entreprise_validation.html index 2f243b24d..d4722756b 100644 --- a/app/templates/entreprises/fiche_entreprise_validation.html +++ b/app/templates/entreprises/fiche_entreprise_validation.html @@ -12,35 +12,58 @@ Adresse : {{ entreprise.adresse }}
    Code postal : {{ entreprise.codepostal }}
    Ville : {{ entreprise.ville }}
    - Pays : {{ entreprise.pays }} + Pays : {{ entreprise.pays }}
    + {% if entreprise.association %} + Association + {% endif %} - {% if correspondants %} -
    - {% for correspondant in correspondants %} +
    + {% if sites %}
    -

    Correspondant

    -
    - Nom : {{ correspondant.nom }}
    - Prénom : {{ correspondant.prenom }}
    - {% if correspondant.telephone %} - Téléphone : {{ correspondant.telephone }}
    - {% endif %} - {% if correspondant.mail %} - Mail : {{ correspondant.mail }}
    - {% endif %} - {% if correspondant.poste %} - Poste : {{ correspondant.poste }}
    - {% endif %} - {% if correspondant.service %} - Service : {{ correspondant.service }}
    +

    Sites

    + {% for site in sites %} +
    + Nom : {{ site.nom }}
    + Adresse : {{ site.adresse }}
    + Code postal : {{ site.codepostal }}
    + Ville : {{ site.ville }}
    + Pays : {{ site.pays }} + {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesCorrespondants, None) %} + {% for correspondant in site.correspondants %} +
    +
    + Civilité : {{ correspondant.civilite|get_civilité }}
    + Nom : {{ correspondant.nom }}
    + Prénom : {{ correspondant.prenom }}
    + {% if correspondant.telephone %} + Téléphone : {{ correspondant.telephone }}
    + {% endif %} + {% if correspondant.mail %} + Mail : {{ correspondant.mail }}
    + {% endif %} + {% if correspondant.poste %} + Poste : {{ correspondant.poste }}
    + {% endif %} + {% if correspondant.service %} + Service : {{ correspondant.service }}
    + {% endif %} + {% if correspondant.origine %} + Origine : {{ correspondant.origine }}
    + {% endif %} + {% if correspondant.notes %} + Notes : {{ correspondant.notes }}
    + {% endif %} +
    +
    + {% endfor %} {% endif %}
    + {% endfor %}
    - {% endfor %} + {% endif %}
    - {% endif %}
    Valider diff --git a/app/templates/entreprises/import_correspondants.html b/app/templates/entreprises/import_correspondants.html deleted file mode 100644 index 796c2135b..000000000 --- a/app/templates/entreprises/import_correspondants.html +++ /dev/null @@ -1,72 +0,0 @@ -{# -*- mode: jinja-html -*- #} -{% extends 'base.html' %} -{% import 'bootstrap/wtf.html' as wtf %} - -{% block styles %} -{{super()}} -{% endblock %} - -{% block app_content %} -

    Importation correspondants

    -
    - -
    -
    -
    -

    - (*) champs requis -

    - {{ wtf.quick_form(form, novalidate=True) }} -
    -
    - - {% if not correspondants_import %} -
    {{ entreprise.siret }}{{ entreprise.siret }} {{ entreprise.nom }} {{ entreprise.adresse }} {{ entreprise.codepostal }}
    {{ data[0].date_fin.strftime('%d/%m/%Y') }} {{ (data[0].date_fin-data[0].date_debut).days//7 }} semaines {{ data[0].type_offre }}{{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }}{{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }} {% if data[0].formation_text %}{{ data[0].formation_text }}{% endif %} {{ data[0].notes }}
    - - - - - - - - - - - -
    AttributTypeDescription
    civilitetextcivilite du correspondant (H ou F)
    nomtextnom du correspondant
    prenomtextprenom du correspondant
    telephonetexttelephone du correspondant
    mailtextmail du correspondant
    postetextposte du correspondant
    servicetextservice dans lequel travaille le correspondant
    originetextorigine du correspondant
    notestextnotes sur le correspondant
    entreprise_sirettextSIRET de l'entreprise
    - {% endif %} - - {% if correspondants_import %} -
    Importation de {{ correspondants_import|length }} correspondant(s)
    - {% for correspondant in correspondants_import %} -
    -
    - Civilité : {{ correspondant.civilite|get_civilité }}
    - Nom : {{ correspondant.nom }}
    - Prénom : {{ correspondant.prenom }}
    - {% if correspondant.telephone %} - Téléphone : {{ correspondant.telephone }}
    - {% endif %} - {% if correspondant.mail %} - Mail : {{ correspondant.mail }}
    - {% endif %} - {% if correspondant.poste %} - Poste : {{ correspondant.poste }}
    - {% endif %} - {% if correspondant.service %} - Service : {{ correspondant.service }}
    - {% endif %} - {% if correspondant.origine %} - Origine : {{ correspondant.origine }}
    - {% endif %} - {% if correspondant.notes %} - Notes : {{ correspondant.notes }}
    - {% endif %} - lien vers l'entreprise -
    -
    - {% endfor %} - {% endif %} -{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/import_donnees.html b/app/templates/entreprises/import_donnees.html new file mode 100644 index 000000000..bbc575e0c --- /dev/null +++ b/app/templates/entreprises/import_donnees.html @@ -0,0 +1,149 @@ +{# -*- mode: jinja-html -*- #} +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} +{% endblock %} + +{% block app_content %} +
    + +
    + +
    +

    {{ title }}

    +
    + +
    +
    +
    +

    + (*) champs requis +

    + {{ wtf.quick_form(form, novalidate=True) }} +
    +
    + + {% if not entreprises_import and not sites_import and not correspondants_import %} +
    Feuille Entreprises
    + + + + + + + + +
    AttributTypeDescription
    siret (*)textsiret de l'entreprise
    nom_entreprise (*)textnom de l'entreprise
    adresse (*)textadresse de l'entreprise
    ville (*)textville de l'entreprise
    code_postal (*)textcode postal de l'entreprise
    paystextpays de l'entreprise
    +
    Feuille Sites
    + + + + + + + + + + +
    AttributTypeDescription
    siret_entreprise (*)textsiret de l'entreprise
    id_site (*)textid du site (ne rien remplir pour créer un site)
    nom_site (*)textnom du site
    adresse (*)textadresse du site
    ville (*)textville du site
    code_postal (*)textcode postal du site
    pays (*)textpays du site
    +
    Feuille Correspondants (à utiliser pour les modifications)
    + + + + + + + + + + + + +
    AttributTypeDescription
    civilite (*)textcivilite du correspondant (H ou F)
    nom (*)textnom du correspondant
    prenom (*)textprenom du correspondant
    telephone (*)texttelephone du correspondant
    mail (*)textmail du correspondant
    postetextposte du correspondant
    servicetextservice dans lequel travaille le correspondant
    originetextorigine du correspondant
    notestextnotes sur le correspondant
    nom_sitetextnom du site lié au correspondant
    + {% endif %} + + {% if entreprises_import %} +
    Importation de {{ entreprises_import|length }} entreprise(s)
    + {% for entreprise in entreprises_import %} +
    +
    + SIRET : {{ entreprise.siret }}
    + Nom : {{ entreprise.nom }}
    + Adresse : {{ entreprise.adresse }}
    + Code postal : {{ entreprise.codepostal }}
    + Ville : {{ entreprise.ville }}
    + Pays : {{ entreprise.pays }}
    + Fiche entreprise +
    + {% for site in entreprise.sites %} +
    + Nom : {{ site.nom }}
    + Adresse : {{ site.adresse }}
    + Code postal : {{ site.codepostal }}
    + Ville : {{ site.ville }}
    + Pays : {{ site.pays }} +
    + {% endfor %} +
    + {% endfor %} + {% endif %} + + {% if sites_import %} +
    Importation de {{ sites_import|length }} site(s)
    + {% for site in sites_import %} +
    + Nom : {{ site.nom }}
    + Adresse : {{ site.adresse }}
    + Code postal : {{ site.codepostal }}
    + Ville : {{ site.ville }}
    + Pays : {{ site.pays }}
    + Fiche entreprise +
    + {% endfor %} + {% endif %} + + {% if correspondants_import %} +
    Importation de {{ correspondants_import|length }} correspondant(s)
    + {% for correspondant in correspondants_import %} +
    +
    + Civilité : {{ correspondant.civilite|get_civilité }}
    + Nom : {{ correspondant.nom }}
    + Prénom : {{ correspondant.prenom }}
    + {% if correspondant.telephone %} + Téléphone : {{ correspondant.telephone }}
    + {% endif %} + {% if correspondant.mail %} + Mail : {{ correspondant.mail }}
    + {% endif %} + {% if correspondant.poste %} + Poste : {{ correspondant.poste }}
    + {% endif %} + {% if correspondant.service %} + Service : {{ correspondant.service }}
    + {% endif %} + {% if correspondant.origine %} + Origine : {{ correspondant.origine }}
    + {% endif %} + {% if correspondant.notes %} + Notes : {{ correspondant.notes }}
    + {% endif %} + Fiche entreprise +
    +
    + {% endfor %} + {% endif %} +
    + +{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/import_entreprises.html b/app/templates/entreprises/import_entreprises.html deleted file mode 100644 index 0b6336603..000000000 --- a/app/templates/entreprises/import_entreprises.html +++ /dev/null @@ -1,52 +0,0 @@ -{# -*- mode: jinja-html -*- #} -{% extends 'base.html' %} -{% import 'bootstrap/wtf.html' as wtf %} - -{% block styles %} -{{super()}} -{% endblock %} - -{% block app_content %} -

    Importation entreprises

    -
    - -
    -
    -
    -

    - (*) champs requis -

    - {{ wtf.quick_form(form, novalidate=True) }} -
    -
    - - {% if not entreprises_import %} - - - - - - - - -
    AttributTypeDescription
    sirettextsiret de l'entreprise
    nom_entreprisetextnom de l'entreprise
    adressetextadresse de l'entreprise
    villetextville de l'entreprise
    code_postaltextcode postal de l'entreprise
    paystextpays de l'entreprise
    - {% endif %} - - {% if entreprises_import %} -
    Importation de {{ entreprises_import|length }} entreprise(s)
    - {% for entreprise in entreprises_import %} -
    -
    - SIRET : {{ entreprise.siret }}
    - Nom : {{ entreprise.nom }}
    - Adresse : {{ entreprise.adresse }}
    - Code postal : {{ entreprise.codepostal }}
    - Ville : {{ entreprise.ville }}
    - Pays : {{ entreprise.pays }} -
    -
    - {% endfor %} - {% endif %} -{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/logs.html b/app/templates/entreprises/logs.html index c6cba95f4..2072c4a84 100644 --- a/app/templates/entreprises/logs.html +++ b/app/templates/entreprises/logs.html @@ -2,6 +2,17 @@ {% extends 'base.html' %} {% block app_content %} + +

    Dernières opérations

    {% if logs.items %} diff --git a/app/templates/entreprises/logs_entreprise.html b/app/templates/entreprises/logs_entreprise.html index cebea0405..d54c4d0af 100644 --- a/app/templates/entreprises/logs_entreprise.html +++ b/app/templates/entreprises/logs_entreprise.html @@ -2,6 +2,20 @@ {% extends 'base.html' %} {% block app_content %} + +

    Dernières opérations - {{ entreprise.nom }}

    {% if logs.items %} diff --git a/app/templates/entreprises/nav.html b/app/templates/entreprises/nav.html index 3cd9636dc..3df51b87b 100644 --- a/app/templates/entreprises/nav.html +++ b/app/templates/entreprises/nav.html @@ -1,14 +1,16 @@ {# -*- mode: jinja-html -*- #} - \ No newline at end of file +
    + +
    \ No newline at end of file diff --git a/app/templates/entreprises/offres_expirees.html b/app/templates/entreprises/offres_expirees.html index d4bc2e415..f0bd4b2f6 100644 --- a/app/templates/entreprises/offres_expirees.html +++ b/app/templates/entreprises/offres_expirees.html @@ -2,6 +2,20 @@ {% extends 'base.html' %} {% block app_content %} + +

    Offres expirées - {{ entreprise.nom }}

    {% if offres_expirees %} diff --git a/app/templates/entreprises/preferences.html b/app/templates/entreprises/preferences.html index 625143728..39b45de8b 100644 --- a/app/templates/entreprises/preferences.html +++ b/app/templates/entreprises/preferences.html @@ -5,11 +5,13 @@ {% block app_content %} {% include 'entreprises/nav.html' %} -

    Préférences module gestion entreprises

    -
    -
    -
    - {{ wtf.quick_form(form, novalidate=True) }} +
    +

    Préférences module gestion entreprises

    +
    +
    +
    + {{ wtf.quick_form(form, novalidate=True) }} +
    {% endblock %} \ No newline at end of file diff --git a/migrations/versions/0b337376e9f7_ajout_taxe_apprentissage_association_.py b/migrations/versions/0b337376e9f7_ajout_taxe_apprentissage_association_.py new file mode 100644 index 000000000..46b7bee63 --- /dev/null +++ b/migrations/versions/0b337376e9f7_ajout_taxe_apprentissage_association_.py @@ -0,0 +1,76 @@ +"""ajout taxe apprentissage, association, changement historique + +Revision ID: 0b337376e9f7 +Revises: ee21c76c8183 +Create Date: 2022-07-04 21:10:53.946385 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "0b337376e9f7" +down_revision = "ee21c76c8183" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "are_historique", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("authenticated_user", sa.Text(), nullable=True), + sa.Column("entreprise_id", sa.Integer(), nullable=True), + sa.Column("object", sa.Text(), nullable=True), + sa.Column("object_id", sa.Integer(), nullable=True), + sa.Column("text", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "are_taxe_apprentissage", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("entreprise_id", sa.Integer(), nullable=True), + sa.Column("annee", sa.Integer(), nullable=True), + sa.Column("montant", sa.Integer(), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["entreprise_id"], ["are_entreprises.id"], ondelete="cascade" + ), + sa.PrimaryKeyConstraint("id"), + ) + op.drop_table("are_logs") + op.add_column( + "are_entreprises", sa.Column("association", sa.Boolean(), nullable=True) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("are_entreprises", "association") + op.create_table( + "are_logs", + sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column( + "date", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=True, + ), + sa.Column("authenticated_user", sa.TEXT(), autoincrement=False, nullable=True), + sa.Column("object", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("text", sa.TEXT(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("id", name="are_logs_pkey"), + ) + op.drop_table("are_taxe_apprentissage") + op.drop_table("are_historique") + # ### end Alembic commands ###