from datetime import date from decimal import Decimal, InvalidOperation from django.http import JsonResponse from django.shortcuts import redirect, render from django.urls import reverse from django.utils import timezone from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db.models import Sum from django.forms.models import model_to_dict from gestion_employe.forms import AffectationForm from gestion_employe.models import Affectation, Employe from gestion_projet.forms import ProjetForm from gestion_projet.models import Projet from .models import ( DocumentProjet, Bailleur, FinancementProjet, ActiviteProjet, LivrablesLivres, ) from .forms import ( ActiviteProjetForm, DocumentProjetForm, FinancementProjetFrom, BailleurForm, LivrablesLivresForm ) def liste_projet(request): """ Vue pour retourner la liste de tous les projet """ projets = Projet.objects.all().order_by('-created_at') data = [] for p in projets: financement = FinancementProjet.objects.filter(projet=p).select_related('bailleur') data.append({ "id_projet": p.id_projet, "nom_projet": p.nom_projet, "date_debut": p.date_debut, "date_fin": p.date_fin, "numero_convention": p.numero_convention, "description": p.description, "type_projet": p.type_projet, "budget": p.budget, "budget_RH": p.budget_RH, "created_at": p.created_at, "statut": p.statut, "avancement": p.avancement, "domaine_recherche": [d.nom for d in p.domaine_recherche.all()], "source_financement": [f.bailleur.nom_organisme for f in financement], }) return JsonResponse(data, safe=False) def liste_employes_affectes(request, projet_id): """ Vue pour retourner la liste des employés affectés à un projet spécifique """ employes = Employe.objects.filter(affectation__projet_id=projet_id).distinct() data = [] for employe in employes: data.append({ "employe": f"{employe.user.first_name} {employe.user.last_name}", "pourcentage_affectation": Affectation.objects.get(employe=employe, projet__id_projet=projet_id).pourcentage_temps_affectation }) return JsonResponse(data, safe=False) def liste_bailleurs(request, projet_id): """ Vue pour retourner la liste des bailleurs associés à un projet spécifique """ bailleurs = FinancementProjet.objects.filter(projet_id=projet_id).select_related('bailleur') data = [] for b in bailleurs: data.append({ "bailleur": b.bailleur.nom_organisme, "pourcentage_financement": b.pourcentage }) return JsonResponse(data, safe=False) @login_required def index(request): projets = Projet.objects.all().order_by('-created_at') nombre_personnel = Affectation.objects.values('employe_id').distinct().count() budget_total = sum([projet.budget for projet in projets if projet.budget or 0]) context = { 'form': AffectationForm(), 'form_ajout_financement': FinancementProjetFrom(), 'form_ajout_bailleur': BailleurForm(), 'bailleurs': Bailleur.objects.all(), 'nombre_personnel': nombre_personnel, 'budget_total': budget_total, 'formulaire_creation_projet': ProjetForm(), 'projet_en_cours': Projet.objects.filter(date_fin__gte=date.today()).count(), } return render(request, 'gestion_projet/index.html', context) @login_required def creation_projet(request): """Vue pour créer un nouveau projet via un formulaire""" formulaire_creation_projet = ProjetForm() if request.method == "POST": form = ProjetForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Projet créé avec succès.") else: messages.error(request, "Le formulaire transmis est invalide.") else: form = ProjetForm() return render( request, "gestion_projet/creation_projet.html", { "formulaire_creation_projet": formulaire_creation_projet } ) @login_required def mises_a_jour_projet(request): """ Vue de mises à jour des informations du projet """ if request.method == "POST": try: projet = Projet.objects.get(id_projet = request.POST["id_projet"]) except Projet.DoesNotExist: messages.error(request, "Ce projet n'existe pas.") else: projet_form = ProjetForm(request.POST, instance=projet) if projet_form.is_valid(): projet_form.save() messages.success(request, f"Le projet d'identifiant {request.POST['id_projet']} a été mis à jour avec succès.") else: messages.error(request, f"Les informations de modification transmises pour la modification du projet {request.POST['id_projet']} ne sont pas valides.") else: messages.error(request, "La méthode de transmission des données n'est pas valide.") return redirect('gestion_projet:index') @login_required def creation_bailleur(request): form = BailleurForm(request.POST) if request.method == 'POST': if form.is_valid(): form.save() return JsonResponse({'success': True}) return JsonResponse({'success': False}) @login_required def liste_bailleur(request): """ Vue pour retourner la liste de tous les bailleurs """ bailleurs = Bailleur.objects.all().order_by('-id') data = [] for b in bailleurs: data.append({ "id": b.id, "nom_organisme": b.nom_organisme, "contact": b.contact, "email": b.email, "pays": b.pays, }) return JsonResponse(data, safe=False) @login_required def ajouter_financement_projet(request): """Ajoute un financement à un projet en vérifiant que le total ne dépasse pas 100%""" if request.method == 'POST': pourcentage_recuperer = request.POST.get('pourcentage') bailleur_id = request.POST.get('bailleur') projet_id = request.POST.get('projet') try: projet = Projet.objects.get(id_projet=projet_id) except Projet.DoesNotExist: return JsonResponse({'success': False, 'message': "Le projet spécifié n’existe pas."}) try: pourcentage_nouveau = Decimal(pourcentage_recuperer) if pourcentage_recuperer else Decimal(0) except (InvalidOperation, TypeError): return JsonResponse({'success': False, 'message': "Le pourcentage saisi n’est pas valide."}) financement_total_actuel = sum(financement.pourcentage for financement in FinancementProjet.objects.filter(projet=projet)) if financement_total_actuel + pourcentage_nouveau > 100: return JsonResponse({'success': False, 'message': "Le total des financements dépasse 100%."}) if bailleur_id: FinancementProjet.objects.create( projet=projet, bailleur_id=bailleur_id, pourcentage=pourcentage_nouveau ) return JsonResponse({'success': True, 'message': "Financement ajouté avec succès."}) else: return JsonResponse({'success': False, 'message': "Aucun bailleur sélectionné."}) return JsonResponse({'success': False, 'message': "Requête invalide."}) @login_required def modification_projet(request, projet_id): """Vue pour éditer un projet existant via un formulaire pré-rempli""" try: projet = Projet.objects.get(id=projet_id) except Projet.DoesNotExist: messages.error(request, "Le projet spécifié n’existe pas.") return redirect('projet-index') if request.method == "POST": form = ProjetForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Le projet a été modifié avec succès.") return redirect('projet-index') messages.error(request, "Erreur lors de la modification du projet.") form = ProjetForm(instance=projet) return render(request, 'gestion_projet/projet-edit.html', {'form': form, 'projets': projet}) @login_required def suppression_projet(request, id): """Vue pour supprimer un projet spécifique après confirmation de l'utilisateur""" try: projet = Projet.objects.get(id=id) except Projet.DoesNotExist: messages.error(request, "Le projet spécifié n’existe pas.") return redirect('projet-index') if request.method == "POST": projet.delete() messages.success(request, "Le projet a été supprimé avec succès.") return redirect('projet-index') @login_required def affecter_employe_projet(request, projet_id): """Vue pour affecter un employé à un projet avec vérification des contraintes d'affectation""" try: projet = Projet.objects.get(id=projet_id) except Projet.DoesNotExist: messages.error(request, "Le projet spécifié n’existe pas.") return redirect('projet-index') if request.method == 'POST': form = AffectationForm(request.POST) if form.is_valid(): employe = Employe.objects.get(id=form.cleaned_data['employe'].id) date_fin_affectation = form.cleaned_data['date_fin_daffectation'] temps_nouveau = form.cleaned_data['temps_affectation'] date_affectation = form.cleaned_data['date_affectation'] if (date_fin_affectation and date_affectation): total_affectation = ( Affectation.objects.filter(employe=employe) .aggregate(total_pourcentage_affectation=Sum('temps_affectation')) ['total_pourcentage_affectation'] or 0 ) if (date_fin_affectation < date_affectation): messages.warning(request, "La date de fin d'affectation ne peut pas être antérieure à la date de début.") return redirect('projet-index') elif date_fin_affectation > projet.date_fin: messages.warning(request, f"La date de fin de l'affectation ({date_fin_affectation}) ne peut pas dépasser la date de fin du projet ({projet.date_fin}).") return redirect('projet-index') elif total_affectation + temps_nouveau > 100: messages.warning( request, f"Les pourcentages d'affectation de l'employé {employe.first_name} {employe.last_name} dépasse 100% sur les différents projets ({total_affectation + temps_nouveau}%)." ) return redirect('projet-index') Affectation.objects.update_or_create( projet=projet, employe=employe, defaults={ 'date_affectation': form.cleaned_data['date_affectation'], 'date_fin_daffectation': date_fin_affectation, 'role': form.cleaned_data['role'], 'temps_affectation': temps_nouveau } ) form = AffectationForm(initial={'projet': projet}) messages.error(request, "Erreur : Formulaire non valide.") return redirect('projet-index') def modifier_financement_projet(request, financement_id): try: financement = FinancementProjet.objects.get(id=financement_id) except FinancementProjet.DoesNotExist: messages.error(request, "Le financement spécifié n’existe pas.") return redirect('projet-index') projet = financement.projet if request.method == 'POST': try: nouveau_pourcentage = Decimal(request.POST.get('pourcentage', '0')) except InvalidOperation: messages.error(request, "Le pourcentage saisi est invalide.") return redirect('projet-index') pourcentage_total_financement = ( FinancementProjet.objects.filter(projet=projet) .exclude(id=financement.id) .aggregate(total_financement=Sum('pourcentage'))['pourcentage_total_financement'] or 0 ) if pourcentage_total_financement + nouveau_pourcentage > 100: messages.error(request, f"Le total des financements dépasse 100% ({pourcentage_total_financement + nouveau_pourcentage}%).") return redirect('projet-index') financement.pourcentage = nouveau_pourcentage financement.save() messages.success(request, "Financement modifié avec succès.") return redirect('projet-index') @login_required def activites_projet(request): try: employe = Employe.objects.get(user=request.user) except Employe.DoesNotExist: messages.error(request, "Impossible d'accéder au menu 'Suivi des activités' car votre profil Utilisateur n'est lié à aucun profil Employe. Veuillez contacter l'administrateur.") return redirect("gestion_conges:conge") try: Affectation.objects.get(employe=employe, date_fin_daffectation__gte = timezone.now().date(), role='chef_projet') except Affectation.DoesNotExist : messages.error(request, "Seuls les chefs de projet ont accès à l'onglet 'Suivi des Activités'") return redirect("gestion_conges:conge") projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first() if projet : context = { **model_to_dict(projet), "nom_projet": projet.projet.nom_projet, "budget_total": projet.projet.budget, "budget_RH": projet.projet.budget_RH, "form_ajout_activite": ActiviteProjetForm(), "form_ajout_document": DocumentProjetForm(), "form_ajout_livrable": LivrablesLivresForm(), } else : context = { "form_ajout_activite": ActiviteProjetForm(), "form_ajout_document": DocumentProjetForm(), "form_ajout_livrable": LivrablesLivresForm(), } return render(request, 'gestion_projet/suivi_activite.html', context) @login_required def ajouter_activite_projet(request): """Vue pour ajouter une activité à un projet spécifique via un formulaire""" employe = Employe.objects.get(user=request.user) projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first() if request.method == "POST": form = ActiviteProjetForm(request.POST) if form.is_valid(): activite = form.save(commit=False) activite.projet = projet.projet activite.budget_depense = request.POST["budget_prevu"] activite.save() messages.success(request, "Activité ajoutée avec succès !") else: messages.error(request, "Erreur : vérifiez les informations saisies.") return redirect('gestion_projet:activites-projet') @login_required def liste_activites_projet(request): """Vue pour retourner la liste des activités d'un projet spécifique""" employe = Employe.objects.get(user=request.user) projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first() if projet: activites = ActiviteProjet.objects.filter(projet_id=projet.projet.id_projet).order_by('-date_debut') else: activites = [] data = [] for a in activites: data.append({ "id": a.id, "titre": a.titre, "date_debut": a.date_debut, "date_fin": a.date_fin, "statut": a.statut, "budget_prevu": a.budget_prevu, "budget_depense": a.budget_depense, "motif_changement_budget": a.motif_changement_budget, "besoin_ressource_materielle": a.besoin_ressource_materielle, "description": a.description, }) return JsonResponse(data, safe=False) @login_required def liste_livrables_activite(request, activite_id): """Vue pour retourner la liste des livrables attendus d'une activité spécifique""" livrables = LivrablesLivres.objects.filter(activite__id=activite_id) data = [] for livrable in livrables: print(livrable.fichier.url) data.append({ "titre": livrable.nom, "lien": livrable.fichier.url if livrable.fichier else "", }) return JsonResponse(data, safe=False) @login_required def mises_a_jour_depense_activite(request): """Vue pour retourner la liste des activités d'un projet spécifique avec leurs dépenses mises à jour""" if request.method == "POST": activite_id = request.POST.get("id_activite") budget_depense = request.POST.get("budget_depense") motif = request.POST.get("motif", "").strip() try: activite = ActiviteProjet.objects.get(id=activite_id) activite.budget_depense = Decimal(budget_depense) if Decimal(budget_depense) != activite.budget_prevu: activite.motif_changement_budget = motif else: activite.motif_changement_budget = "" activite.save() messages.success(request, f"Dépenses mises à jour pour l’activité '{activite.titre}'.") except (ActiviteProjet.DoesNotExist, InvalidOperation): messages.error(request, "Erreur lors de la mise à jour des dépenses.") return redirect("gestion_projet:activites-projet") @login_required def ajouter_livrables_projet(request): """Vue pour ajouter un livrable à une activité de projet spécifique via un formulaire""" if request.method == "POST": form = LivrablesLivresForm(request.POST, request.FILES) if form.is_valid(): form.save() messages.success(request, "Livrable ajouté avec succès !") else: messages.error(request, "Erreur : vérifiez les informations saisies.") return redirect('gestion_projet:activites-projet') @login_required def ajouter_document_projet(request): """Ajoute un document à un projet""" employe = Employe.objects.get(user=request.user) projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first() if request.method == "POST": form = DocumentProjetForm(request.POST, request.FILES) if form.is_valid(): document = form.save(commit=False) document.projet = projet.projet document.save() messages.success(request, "Document ajouté avec succès !") else: messages.error(request, "Erreur : le document n’a pas pu être enregistré.") return redirect('gestion_projet:activites-projet') def liste_documents_projet(request): employe = Employe.objects.get(user=request.user) projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first() if projet: documents = DocumentProjet.objects.filter(projet__id_projet=projet.projet.id_projet) else: documents = [] data = [] for d in documents: data.append({ "nom_document": d.nom_document, "numero": d.numero, "date_validite": d.date_validite, "lien_document": d.fichier.url if d.fichier else "", }) return JsonResponse(data, safe=False) def modifier_activite_projet(request, id): """Vue pour modifier une activité de projet spécifique via un formulaire pré-rempli""" try: activite = ActiviteProjet.objects.get(id=id) except ActiviteProjet.DoesNotExist: messages.error(request, "L'activité spécifiée n’existe pas.") return redirect('activites-projet') if request.method == 'POST': form = ActiviteProjetForm(request.POST, instance=activite) if form.is_valid(): activite.besoin_ressource_materielle = bool(request.POST.get("besoin_ressource_materielle")) form.save() messages.success(request, f"Activité « {activite.titre} » modifiée avec succès.") else: messages.error(request, "Erreur lors de la modification de l'activité.") return redirect('activites-projet') form = ActiviteProjetForm(instance=activite) return render( request, 'gestion_projet/activite.html', { 'form': form, 'activite': activite, } ) def annuler_activite_projet(request): """Vue pour annuler une activité de projet spécifique après confirmation de l'utilisateur""" print(request.POST) if request.method != "POST": messages.error(request, "Requête invalide.") return redirect('gestion_projet:activites-projet') try: activite = ActiviteProjet.objects.get(id=request.POST.get('id_activite')) except ActiviteProjet.DoesNotExist: messages.error(request, "L'activité spécifiée n’existe pas.") return redirect('gestion_projet:activites-projet') if request.method == "POST": activite.annuler = True activite.motif_annulation = request.POST.get("motif_annulation", "").strip() activite.save() messages.success(request, f"L'activité '{activite.titre}' a été annulée avec succès.") return redirect('gestion_projet:activites-projet')