from datetime import date from decimal import Decimal, InvalidOperation from django.http import JsonResponse from django.shortcuts import redirect, render 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')