from django import forms from django.forms import Textarea, DateInput from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from django.db.models import Q from .models import Project, ProjectUserAccess, ProjectTeamAccess, ProjectDocument, ProjectPhase from common.models import UserConfig User = get_user_model() class MultipleFileInput(forms.ClearableFileInput): allow_multiple_selected = True class MultipleFileField(forms.FileField): def __init__(self, *args, **kwargs): kwargs.setdefault("widget", MultipleFileInput()) super().__init__(*args, **kwargs) def clean(self, data, initial=None): single_file_clean = super().clean if isinstance(data, (list, tuple)): result = [single_file_clean(d, initial) for d in data] else: result = [single_file_clean(data, initial)] return result class ProjectUserAccessForm(forms.ModelForm): user = forms.ModelChoiceField( queryset=User.objects.none(), label=_("Utilisateur"), widget=forms.Select(attrs={"class": "form-select", 'id': 'id_member_select'}) ) can_view = forms.BooleanField( required=False, initial=True, label=_("Peut consulter"), widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) can_edit = forms.BooleanField( required=False, initial=False, label=_("Peut éditer"), widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) class Meta: model = ProjectUserAccess fields = ['user', 'can_view', 'can_edit'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['can_view'].initial = True if 'user' in self.data: try: user_id = int(self.data.get('user')) self.fields['user'].queryset = User.objects.filter(id=user_id) except (ValueError, TypeError): pass class ProjectTeamAccessForm(forms.ModelForm): can_view = forms.BooleanField( required=False, initial=True, label=_("Peut consulter"), widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) can_edit = forms.BooleanField( required=False, initial=False, label=_("Peut éditer"), widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) class Meta: model = ProjectTeamAccess fields = ['team', 'can_view', 'can_edit'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from contracts.models import CompanyTeam self.fields['team'].queryset = CompanyTeam.objects.select_related('company').order_by('name') self.fields['team'].widget.attrs['class'] = 'form-select' self.fields['team'].label = _("Équipe") self.fields['can_view'].initial = True class ProjectForm(forms.ModelForm): class Meta: model = Project fields = ['name', 'description', 'start_date', 'end_date', 'status', 'thematics', 'tags', 'pmo_number', 'osiris_number'] def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) # on récupère l'utilisateur self.is_update = kwargs.pop('is_update', False) super().__init__(*args, **kwargs) if self.is_update: self.fields.pop('status', None) # on enlève le champ status si c'est un formulaire de modification for field_name, field in self.fields.items(): base_class = 'form-control' if not isinstance(field.widget, forms.Select) else 'form-select' # Ajout d’un style spécifique si le champ est facultatif if not field.required: style = 'border: 1px dashed #6c757d;' # gris Bootstrap else: style = '' field.widget.attrs.update({ 'class': base_class, 'style': style }) if field_name == 'description': field.widget.attrs['rows'] = 3 # Appliquer type="date" pour tous les DateInput if isinstance(field.widget, DateInput): # Force un format ISO pour que la valeur apparaisse dans l'input date field.widget = DateInput(format="%Y-%m-%d", attrs=field.widget.attrs) field.widget.input_type = 'date' # Ajoute le format ISO aux formats acceptés pour l'input existing_formats = list(field.input_formats or []) if "%Y-%m-%d" not in existing_formats: field.input_formats = ["%Y-%m-%d"] + existing_formats # Optionnel : placeholder intelligent # if not field.label: # continue # placeholder = f"{field.label} ({'optionnel' if not field.required else 'obligatoire'})" # field.widget.attrs.setdefault('placeholder', placeholder) # Filtrer les thématiques selon UserConfig # if user: # try: # config = UserConfig.objects.get(user=user) # if config.intervention_thematics.exists(): # self.fields['thematics'].queryset = config.intervention_thematics.all() # except UserConfig.DoesNotExist: # pass class ProjectDocumentForm(forms.ModelForm): documents = MultipleFileField(required=True, label=_("Documents")) class Meta: model = ProjectDocument fields = ['documents'] def __init__(self, *args, **kwargs): super(ProjectDocumentForm, self).__init__(*args, **kwargs) self.fields['documents'].widget.attrs.update({'class': 'form-control'}) class ProjectOrderAssociationForm(forms.Form): order = forms.ModelChoiceField( queryset=None, label=_("Commande"), widget=forms.Select(attrs={"class": "form-select", 'id': 'id_order_select'}), empty_label=_("Sélectionner une commande...") ) def label_from_instance(self, obj): """Personnaliser l'affichage de chaque option dans le select""" return f"{obj.order_code} - {obj.contract.contract_number} - {obj.order_date.strftime('%d/%m/%Y')}" def __init__(self, *args, **kwargs): project = kwargs.pop('project', None) user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Import here to avoid circular imports from contracts.models import ContractOrder from common.models import UserThematics # Filter orders that are in status pending, sent ou completed orders_queryset = ContractOrder.objects.filter( order_status__in=['pending', 'sent', 'completed'] ).select_related('contract', 'contract__company') # Filtrer selon les permissions utilisateur if user and project: try: user_config = UserConfig.objects.get(user=user) # Récupérer les thématiques du projet project_thematics = project.thematics.all() # Récupérer les thématiques pour lesquelles l'utilisateur a can_edit_interventions=True user_thematics_with_edit = UserThematics.objects.filter( user_config=user_config, can_edit_interventions=True ).values_list('thematic_id', flat=True) # Vérifier s'il y a une intersection entre les thématiques du projet # et celles où l'utilisateur peut éditer des interventions allowed_project_thematics = project_thematics.filter( id__in=user_thematics_with_edit ) if not allowed_project_thematics.exists(): # Si l'utilisateur n'a pas le droit d'éditer d'interventions # pour les thématiques du projet, on vide le queryset orders_queryset = ContractOrder.objects.none() else: # Filtrer par contrats autorisés si l'utilisateur a des restrictions if user_config.limit_interventions_to_contracts: allowed_contracts = user_config.contracts.all() if allowed_contracts.exists(): orders_queryset = orders_queryset.filter(contract__in=allowed_contracts) else: # Si limit_interventions_to_contracts est activé mais aucun contrat autorisé orders_queryset = ContractOrder.objects.none() except UserConfig.DoesNotExist: # Si pas de config utilisateur, on limite l'accès orders_queryset = ContractOrder.objects.none() self.fields['order'].queryset = orders_queryset.order_by('-order_date') self.fields['order'].label_from_instance = self.label_from_instance class ProjectPhaseForm(forms.ModelForm): class Meta: model = ProjectPhase fields = ['name', 'description', 'start_date', 'end_date', 'contract'] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'start_date': DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'end_date': DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'contract': forms.Select(attrs={'class': 'form-select'}), } labels = { 'name': _("Nom"), 'description': _("Description"), 'start_date': _("Date de début"), 'end_date': _("Date de fin"), 'contract': _("Contrat"), } def __init__(self, *args, user=None, **kwargs): super().__init__(*args, **kwargs) # Le contrat est optionnel self.fields['contract'].required = False self.fields['contract'].empty_label = _("Aucun contrat") # Filtrer les contrats selon les permissions utilisateur if user: from contracts.permissions import get_allowed_contracts_for_user self.fields['contract'].queryset = get_allowed_contracts_for_user(user) # Appliquer les styles pour les champs optionnels for field_name, field in self.fields.items(): if not field.required: current_class = field.widget.attrs.get('class', '') field.widget.attrs['class'] = f"{current_class} optional-field".strip()