233 lines
No EOL
7.8 KiB
Python
233 lines
No EOL
7.8 KiB
Python
from django import forms
|
|
from django.forms import DateInput, formset_factory
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from decimal import Decimal
|
|
|
|
from .models import (
|
|
ContractOrder,
|
|
CONTRACT_ORDER_STATUS_CHOICES,
|
|
ClaimDeclarationAttachment,
|
|
ContractOrderAttachment,
|
|
)
|
|
|
|
from .permissions import get_allowed_contracts_for_user
|
|
|
|
class ContractOrderForm(forms.ModelForm):
|
|
def __init__(self, *args, user=None, project_id=None, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Limiter les contrats visibles par l'utilisateur
|
|
if user is not None:
|
|
self.fields['contract'].queryset = get_allowed_contracts_for_user(user)
|
|
|
|
# Si des interventions existent pour cet ordre → on fige le contrat (UI)
|
|
if self.instance and self.instance.pk:
|
|
has_interventions = self.instance.interventions.exists() # via related_name="interventions"
|
|
if has_interventions:
|
|
self.fields['contract'].disabled = True
|
|
self.fields['contract'].help_text = _("Ce contrat ne peut plus être modifié car des interventions sont déjà liées.")
|
|
|
|
|
|
# Ajout de la classe Bootstrap à tous les champs
|
|
for field_name, field in self.fields.items():
|
|
widget = field.widget
|
|
|
|
base_class = ''
|
|
if isinstance(widget, forms.Select):
|
|
base_class = 'form-select'
|
|
else:
|
|
base_class = 'form-control'
|
|
|
|
# Marquer les champs optionnels avec une classe supplémentaire
|
|
if not field.required:
|
|
widget.attrs['class'] = f"{base_class} optional-field"
|
|
else:
|
|
widget.attrs['class'] = base_class
|
|
|
|
|
|
# Remettre le champ vide si la valeur est 0
|
|
if self.initial.get('total_amount') in [0, 0.0] or self.instance.total_amount in [0, 0.0]:
|
|
self.initial['total_amount'] = ''
|
|
|
|
if self.instance and self.instance.pk:
|
|
self.fields['order_date'].initial = self.instance.order_date.strftime('%Y-%m-%d')
|
|
self.fields['delivery_date'].initial = self.instance.delivery_date.strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
def clean(self):
|
|
cleaned = super().clean()
|
|
# Sécurité serveur : si des interventions existent, interdire toute modif du contrat
|
|
if self.instance and self.instance.pk and self.instance.interventions.exists():
|
|
if 'contract' in self.changed_data:
|
|
self.add_error(
|
|
'contract',
|
|
_("Le contrat ne peut plus être modifié car des interventions sont déjà liées à cette commande.")
|
|
)
|
|
raise ValidationError(_("Modification du contrat interdite."))
|
|
return cleaned
|
|
|
|
|
|
order_date = forms.DateField(
|
|
required=True,
|
|
widget=DateInput(
|
|
attrs={'type': 'date', 'class': 'form-control'},
|
|
format='%Y-%m-%d' # Format ISO compatible avec HTML5
|
|
),
|
|
label=_("Date de la commande")
|
|
)
|
|
delivery_date = forms.DateField(
|
|
required=True,
|
|
widget=DateInput(
|
|
attrs={'type': 'date', 'class': 'form-control'},
|
|
format='%Y-%m-%d'
|
|
),
|
|
label=_("Date de livraison")
|
|
)
|
|
order_status = forms.ChoiceField(
|
|
choices=CONTRACT_ORDER_STATUS_CHOICES,
|
|
widget=forms.Select(attrs={'class': 'form-select'}),
|
|
label=_("Statut de la commande")
|
|
)
|
|
order_file = forms.FileField(
|
|
required=False,
|
|
widget=forms.ClearableFileInput(attrs={'class': 'form-control'}),
|
|
label=_("Fichier de commande")
|
|
)
|
|
total_amount = forms.DecimalField(
|
|
required=False,
|
|
label=_("Montant total (€)"),
|
|
widget=forms.NumberInput(attrs={
|
|
'step': '0.01',
|
|
'placeholder': 'ex. 1000.00',
|
|
})
|
|
)
|
|
|
|
class Meta:
|
|
model = ContractOrder
|
|
fields = [
|
|
'contract',
|
|
'order_code',
|
|
'description',
|
|
'order_date',
|
|
'delivery_date',
|
|
'total_amount',
|
|
'order_status',
|
|
'order_file'
|
|
]
|
|
widgets = {
|
|
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
|
}
|
|
labels = {
|
|
'contract': _("Contrat"),
|
|
'order_code': _("Référence de commande"),
|
|
'description': _("Description"),
|
|
}
|
|
|
|
|
|
class ContractOrderQuoteQuantityForm(forms.Form):
|
|
contract_post_id = forms.IntegerField(
|
|
min_value=1,
|
|
widget=forms.HiddenInput(),
|
|
)
|
|
quantity = forms.DecimalField(
|
|
label=_("Quantité"),
|
|
decimal_places=2,
|
|
max_digits=10,
|
|
min_value=Decimal('0'),
|
|
required=False,
|
|
widget=forms.NumberInput(
|
|
attrs={
|
|
'class': 'form-control form-control-sm text-end quote-quantity-input',
|
|
'step': '1',
|
|
'min': '0',
|
|
'inputmode': 'decimal',
|
|
'style': 'text-align: center;',
|
|
}
|
|
),
|
|
)
|
|
|
|
def __init__(self, *args, allowed_post_ids=None, disabled=False, **kwargs):
|
|
self.allowed_post_ids = allowed_post_ids
|
|
self._is_disabled = disabled
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self._is_disabled:
|
|
self.fields['quantity'].disabled = True
|
|
css_class = self.fields['quantity'].widget.attrs.get('class', '')
|
|
self.fields['quantity'].widget.attrs['class'] = f"{css_class} bg-light".strip()
|
|
|
|
|
|
def clean_contract_post_id(self):
|
|
value = self.cleaned_data.get('contract_post_id')
|
|
if not value:
|
|
raise ValidationError(_("Poste invalide."))
|
|
if self.allowed_post_ids is not None and value not in self.allowed_post_ids:
|
|
raise ValidationError(_("Poste invalide."))
|
|
return value
|
|
|
|
def clean_quantity(self):
|
|
quantity = self.cleaned_data.get('quantity')
|
|
if quantity in (None, ''):
|
|
return Decimal('0')
|
|
return quantity
|
|
|
|
|
|
ContractOrderQuoteQuantityFormSet = formset_factory(
|
|
ContractOrderQuoteQuantityForm,
|
|
extra=0,
|
|
)
|
|
|
|
|
|
class ClaimDeclarationAttachmentForm(forms.ModelForm):
|
|
"""Formulaire pour ajouter une pièce jointe à une déclaration de créance"""
|
|
|
|
class Meta:
|
|
model = ClaimDeclarationAttachment
|
|
fields = ['title', 'document']
|
|
widgets = {
|
|
'title': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': _('Ex: Facture fournisseur, Bon de livraison...'),
|
|
}),
|
|
'document': forms.ClearableFileInput(attrs={
|
|
'class': 'form-control',
|
|
}),
|
|
}
|
|
labels = {
|
|
'title': _("Titre du justificatif"),
|
|
'document': _("Fichier"),
|
|
}
|
|
|
|
|
|
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 [item for item in result if item]
|
|
|
|
|
|
class ContractOrderAttachmentForm(forms.ModelForm):
|
|
"""Formulaire pour ajouter des documents (photos, PDF, …) à une commande."""
|
|
files = MultipleFileField(required=True, label=_("Fichiers"))
|
|
|
|
class Meta:
|
|
model = ContractOrderAttachment
|
|
fields = ['files']
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['files'].widget.attrs.update({'class': 'form-control'}) |