243 lines
No EOL
10 KiB
Python
243 lines
No EOL
10 KiB
Python
from django import forms
|
||
from django.utils.translation import get_language, gettext as _
|
||
from django.forms import inlineformset_factory, BaseInlineFormSet
|
||
|
||
from .models import Control, Remark, RemarkImage, ControlReferencePlan, ControlReportTemplate, ControlQuantity
|
||
from projects.models import ProjectPhase
|
||
from common.models import Thematic, UserContractAccess
|
||
from assets.models import AssetCategory
|
||
from contracts.models import Contract
|
||
from interventions.models import Intervention
|
||
|
||
|
||
class ControlForm(forms.ModelForm):
|
||
class Meta:
|
||
model = Control
|
||
fields = [
|
||
'description', 'status', 'control_type',
|
||
'project', 'phase',
|
||
'thematics', 'categories', 'interventions', 'visibility', 'shared_edition',
|
||
'location', 'lon', 'lat', 'geojson'
|
||
]
|
||
widgets = {
|
||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'autocomplete': 'off'}),
|
||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||
'control_type': forms.Select(attrs={'class': 'form-select'}),
|
||
'visibility': forms.Select(attrs={'class': 'form-select'}),
|
||
'shared_edition': forms.CheckboxInput(attrs={'class': 'form-check'}),
|
||
'thematics': forms.CheckboxSelectMultiple(),
|
||
'categories': forms.CheckboxSelectMultiple(),
|
||
'project': forms.Select(attrs={'class': 'form-select', 'id': 'project-select'}),
|
||
'phase': forms.Select(attrs={'class': 'form-select', 'id': 'phase-select'}),
|
||
'location': forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
||
'interventions': forms.SelectMultiple(attrs={'style': 'display:none;'}),
|
||
'lon': forms.HiddenInput(),
|
||
'lat': forms.HiddenInput(),
|
||
'geojson': forms.HiddenInput(),
|
||
}
|
||
labels = {
|
||
'location': _('Localisation') + ' (' + _('facultatif') + ')',
|
||
'categories': _('Catégories') + ' (' + _('facultatif') + ')',
|
||
'interventions': _('Interventions liées') + ' (' + _('facultatif') + ')',
|
||
'project': _('Projet') + ' (' + _('facultatif') + ')',
|
||
'phase': _('Phase du projet') + ' (' + _('facultatif') + ')',
|
||
'description': _('Description') + ' (' + _('facultatif') + ')',
|
||
}
|
||
|
||
def __init__(self, *args, user=None, **kwargs):
|
||
super(ControlForm, self).__init__(*args, **kwargs)
|
||
self.fields['phase'].queryset = ProjectPhase.objects.none()
|
||
self.fields['categories'].queryset = AssetCategory.objects.none()
|
||
|
||
lang = get_language()
|
||
if lang == 'nl':
|
||
self.fields['thematics'].queryset = Thematic.objects.all().order_by('name_nl')
|
||
else:
|
||
self.fields['thematics'].queryset = Thematic.objects.all().order_by('name_fr')
|
||
|
||
# Si un utilisateur est passé au form et qu'il a une thématique par défaut
|
||
if (
|
||
not self.is_bound
|
||
and not self.instance.pk # uniquement à la création
|
||
and user
|
||
and hasattr(user, "config")
|
||
and user.config.default_thematic
|
||
and not self.initial.get('thematics')
|
||
):
|
||
self.initial['thematics'] = [user.config.default_thematic.id]
|
||
|
||
# Vérification des catégories pour la validation
|
||
if 'thematics' in self.data:
|
||
thematic_ids = self.data.getlist('thematics')
|
||
self.fields['categories'].queryset = AssetCategory.objects.filter(
|
||
thematic__id__in=thematic_ids
|
||
).distinct()
|
||
elif self.instance.pk:
|
||
# Si modification d’un objet existant, afficher toutes les catégories possibles
|
||
# en fonction des thématiques du contrôle (pas seulement les sélectionnées)
|
||
thematic_ids = list(self.instance.thematics.values_list('id', flat=True))
|
||
if thematic_ids:
|
||
self.fields['categories'].queryset = AssetCategory.objects.filter(
|
||
thematic__id__in=thematic_ids
|
||
).distinct()
|
||
else:
|
||
self.fields['categories'].queryset = AssetCategory.objects.none()
|
||
# Vérification des phases pour la validation
|
||
if 'project' in self.data and self.data.get('project'):
|
||
project_id = self.data.get('project')
|
||
self.fields['phase'].queryset = ProjectPhase.objects.filter(project_id=project_id)
|
||
elif self.instance.pk and self.instance.project:
|
||
# Si modification d'un objet existant
|
||
self.fields['phase'].queryset = ProjectPhase.objects.filter(project=self.instance.project)
|
||
|
||
# Interventions : éviter de charger toutes les options (liste potentiellement volumineuse)
|
||
if 'interventions' in self.data:
|
||
ids = [x for x in self.data.getlist('interventions') if str(x).strip().isdigit()]
|
||
self.fields['interventions'].queryset = Intervention.objects.filter(id__in=ids)
|
||
elif self.instance.pk:
|
||
self.fields['interventions'].queryset = self.instance.interventions.all()
|
||
else:
|
||
self.fields['interventions'].queryset = Intervention.objects.none()
|
||
|
||
|
||
|
||
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 RemarkForm(forms.ModelForm):
|
||
images = MultipleFileField(required=False)
|
||
|
||
class Meta:
|
||
model = Remark
|
||
fields = ["control", "status", "description", "images", "lon", "lat", "x", "y"]
|
||
|
||
widgets = {
|
||
"description": forms.Textarea(attrs={
|
||
"class": "form-control", # Ajoute la classe Bootstrap
|
||
"rows": 2, # Limite à deux lignes
|
||
}),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super(RemarkForm, self).__init__(*args, **kwargs)
|
||
self.fields['images'].widget.attrs.update({'class': 'form-control'})
|
||
|
||
|
||
class ControlReferencePlanForm(forms.ModelForm):
|
||
class Meta:
|
||
model = ControlReferencePlan
|
||
fields = ["name", "plan"]
|
||
|
||
|
||
class ControlReportTemplateForm(forms.ModelForm):
|
||
class Meta:
|
||
model = ControlReportTemplate
|
||
fields = ['header_notes', 'footer_notes']
|
||
widgets = {
|
||
'header_notes': forms.Textarea(attrs={
|
||
'class': 'form-control',
|
||
'rows': 4,
|
||
'placeholder': _("Entrez les notes de début ..."),
|
||
}),
|
||
'footer_notes': forms.Textarea(attrs={
|
||
'class': 'form-control',
|
||
'rows': 4,
|
||
'placeholder': _("Entrez les notes de fin ..."),
|
||
}),
|
||
}
|
||
|
||
|
||
|
||
class ControlQuantityForm(forms.ModelForm):
|
||
# Champ “recherchable” pour le poste: on utilisera un input visible + un hidden pour l’ID
|
||
contract_post_label = forms.CharField(
|
||
required=False,
|
||
widget=forms.TextInput(attrs={
|
||
"class": "form-control",
|
||
"placeholder": _("Chercher un poste (autocomplete)..."),
|
||
"autocomplete": "off",
|
||
"autocorrect": "off", # (iOS)
|
||
"autocapitalize": "none",
|
||
"spellcheck": "false",
|
||
})
|
||
)
|
||
contract_post_id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||
|
||
class Meta:
|
||
model = ControlQuantity
|
||
fields = [
|
||
"contract", "description",
|
||
"q1", "q2", "q3",
|
||
]
|
||
widgets = {
|
||
"contract": forms.Select(attrs={"class": "form-select", "id": "quantity-contract-select"}),
|
||
"description": forms.TextInput(attrs={"class": "form-control", "placeholder": _("Description si pas de marché")}),
|
||
"q1": forms.NumberInput(attrs={"class": "form-control", "step": "0.01", "inputmode": "decimal"}),
|
||
"q2": forms.NumberInput(attrs={"class": "form-control", "step": "0.01", "inputmode": "decimal"}),
|
||
"q3": forms.NumberInput(attrs={"class": "form-control", "step": "0.01", "inputmode": "decimal"}),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
if not self.is_bound and self.instance and self.instance.pk:
|
||
cp = getattr(self.instance, "contract_post", None)
|
||
if cp:
|
||
# ID caché
|
||
self.fields["contract_post_id"].initial = cp.id
|
||
|
||
# Libellé visible (NL/FR selon la langue active)
|
||
lang = (get_language() or "fr").lower()
|
||
desc = cp.description_nl if lang.startswith("nl") else cp.description_fr
|
||
label = f"{cp.order_number} - {cp.code} - {desc}"
|
||
self.fields["contract_post_label"].initial = label
|
||
|
||
|
||
def clean(self):
|
||
cleaned = super().clean()
|
||
# si contract_post_id est fourni, on le rattache dans la view (post-clean) pour éviter import circulaire
|
||
return cleaned
|
||
|
||
|
||
|
||
class ControlQuantityInlineFormSet(BaseInlineFormSet):
|
||
def __init__(self, *args, user=None, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
qs = Contract.objects.none()
|
||
if user and hasattr(user, "config"):
|
||
allowed_ids = UserContractAccess.objects.filter(
|
||
user_config=user.config, can_view_posts=True
|
||
).values_list("contract_id", flat=True)
|
||
qs = Contract.objects.filter(id__in=allowed_ids).order_by("contract_number")
|
||
|
||
# Injecter le même queryset dans chaque form
|
||
for form in self.forms:
|
||
form.fields["contract"].queryset = qs
|
||
form.fields["contract"].empty_label = _("Sélectionner un marché")
|
||
|
||
|
||
|
||
QuantityFormSet = inlineformset_factory(
|
||
Remark,
|
||
ControlQuantity,
|
||
form=ControlQuantityForm,
|
||
formset=ControlQuantityInlineFormSet,
|
||
fields=["contract", "description", "q1", "q2", "q3"],
|
||
extra=0,
|
||
can_delete=True
|
||
) |