Hello,
I am facing an issue with validation of select fields. Here is the context:
I have the following models:
class Framework_agreement( models.Model):
number = models.CharField('Numéro',max_length=30,)
customer=models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='Client')
class PO_market(models.Model):
number = models.CharField('Numéro',max_length=30,)
customer=models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='Client')
signature_date= models.DateField('Date de signature',null=True,blank=True)
expiration_date = models.DateField('Date de fin',null=True,blank=True)
framework_agreement = models.ForeignKey(Framework_agreement, on_delete=models.SET_NULL, null=True, blank=True,\
verbose_name="Accord-cadre")
class Business(models.Model):
name = models.CharField('Affaire',max_length=200, null = True, blank=True)
customer = models.ForeignKey(Company,related_name = 'customer',on_delete=models.CASCADE,verbose_name='Client')
beneficiary = models.ForeignKey(Company,related_name = 'beneficiary', on_delete=models.CASCADE,verbose_name='Bénéficiaire',
null = True,blank = True)
customer_ref = models.CharField('Référence client', max_length=250, default="",
null=True, blank=True)
engagement_number= models.CharField("N° d'engagement", max_length=250, default="",
null=True, blank=True)
organisational_unit= models.CharField("Direction", max_length=250, default="",
null=True, blank=True)
framework_agreement = models.ForeignKey(Framework_agreement, on_delete=models.SET_NULL, verbose_name='Accord-cadre', null=True, blank=True)
po_market = models.ForeignKey(PO_market, on_delete=models.SET_NULL, verbose_name='Marché à bons de commande', null=True, blank=True)
I have a the following view
class BusinessUpdateView(LoginRequiredMixin, UpdateView):
model = Business
form_class = BusinessForm
template_name = 'salesforecast/business_form.html'
def get_form(self, form_class=None):
form = super().get_form(form_class)
return form
def get_initial(self):
initial = super().get_initial()
return initial
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['business_form'] = context['form']
if self.object.ponderation.facturable is False and self.object.ponderation.final:
InvoiceFormSet = inlineformset_factory(Business, Invoice,
form=InvoiceInLineForm,
can_delete=False,
extra=0)
else:
InvoiceFormSet = inlineformset_factory(Business, Invoice,
form=InvoiceInLineForm,
can_delete=True,
extra=1)
if self.request.POST:
context['invoice_formset'] = InvoiceFormSet(self.request.POST, instance=self.object)
else:
context['invoice_formset'] = InvoiceFormSet(instance=self.object)
return context
def form_invalid(self, form):
print("valaeur iniitale de po_market:", self.initial.get('po_market'))
print("valeur saisie de po_market:", self.request.POST.get('po_market'))
print("valeur iniitale de framework_agreement:", self.initial.get('framework_agreement'))
print("valeur saisie de framework_agreement:", self.request.POST.get('framework_agreement'))
return super().form_invalid(form)
def form_valid(self, form):
print("valaeur iniitale de po_market:", self.initial.get('po_market'))
print("valeur saisie de po_market:", self.request.POST.get('po_market'))
# On met à jour l'objet business
response = super().form_valid(form)
context = self.get_context_data()
invoices = context['invoice_formset']
with transaction.atomic():
self.object = form.save()
if invoices.is_valid():
invoices.instance = self.object
invoices.save() # le save inclut l'initialisation de contact, addressee, address et comment à partir de business
# On informe le user de l'impact du changement de framework_agreement et po_market
msg = ""
if form.has_changed() or invoices.has_changed():
msg = "Les modifications ont été enregistrées."
if ('initial_framework_agreement') in form.changed_data:
if self.object.framework_agreement:
msg += f'\nL\'affaire a été rattachée à l\'accord cadre "{self.object.framework_agreement}"'
else:
msg += f'\nL\'affaire a été détachée du contrat cadre "{self.object.framework_agreement}"'
if ('po_market' in form.changed_data):
if self.object.po_market:
msg += f'\nL\'affaire a été rattachée au marché à bons de commande "{self.object.po_market}"'
else:
msg += f'\nL\'affaire a été détachée du marché à bons de commande "{self.object.po_market}"'
if self.object.collected_amount() == self.object.amount:
final_status = BusinessStatus.objects.filter(weight=100, final=True).first()
print("Status:",final_status.label)
self.object.ponderation = final_status
self.object.save()
msg += "\nToutes les factures ont été émises et encaissées. L'affaire est terminée."
if msg != "":
messages.success(self.request, msg)
return response
which is based on the following form:
class BusinessForm(ModelForm):
def clean_framework_agreement(self):
customer = self.cleaned_data.get('customer')
framework_agreement = self.cleaned_data.get('framework_agreement')
print(f'METHODE clean_framework_agreement - customer: {customer}, fa:{framework_agreement}')
# Mettez à jour les options de framework_agreement en fonction de la valeur actuelle de customer
self.fields['framework_agreement'].queryset = Framework_agreement.objects.filter(customer=customer)
# On s'assure que la valeur soumise par l'utilisateur est dans les options acceptables
if framework_agreement not in self.fields['framework_agreement'].queryset and framework_agreement is not None:
raise forms.ValidationError("La valeur du champ n'est pas valide.")
return framework_agreement
def clean_po_market(self):
customer = self.cleaned_data.get('customer')
framework_agreement = self.cleaned_data.get('framework_agreement')
po_market = self.cleaned_data.get('po_market')
print(f'METHODE clean_po_market - customer: {customer}, fa:{framework_agreement}, po:{po_market}',)
# Mettez à jour les options de po_market en fonction de la valeur actuelle de framework_agreement
if framework_agreement:
self.fields['po_market'].queryset = PO_market.objects.filter(framework_agreement=framework_agreement)
print("po_market queryset:", PO_market.objects.filter(framework_agreement=framework_agreement))
else:
self.fields['po_market'].queryset = PO_market.objects.filter(customer=customer, framework_agreement=None)
# On s'assure que la valeur soumise par l'utilisateur est dans les options acceptables
if po_market not in self.fields['po_market'].queryset and po_market is not None:
raise forms.ValidationError("La valeur du champ po_market n'est pas valide.")
return po_market
def clean(self):
cleaned_data = super().clean()
print("PO MARKET Avant clean:", cleaned_data.get('po_market') )
self.clean_framework_agreement()
self.clean_po_market()
print("Exécution de la méthode CLEAN")
name = cleaned_data.get('name')
amount = cleaned_data.get('amount')
ponderation = cleaned_data.get('ponderation')
framework_agreement = cleaned_data.get('framework_agreement')
print("framework_agreement clean exécuté")
po_market = cleaned_data.get('po_market')
print("po_market clean exécuté")
customer = cleaned_data.get('customer')
beneficiary = cleaned_data.get('beneficiary')
account_exec = cleaned_data.get('account_exec')
signature_date = cleaned_data.get('signature_date')
vat_rate = cleaned_data.get('vat_rate')
if not(name):
msg = ('Veuillez compléter le nom de l\'affaire')
self.add_error('name', msg)
if not (amount):
msg = ('Veuillez définir le montant de l\'affaire')
self.add_error('amount', msg)
if 'account_exec' in self.fields:
if not (account_exec):
msg = ('Veuillez choisir le chargé d\'affaire')
self.add_error('account_exec', msg)
if 'signature_date' in self.fields:
if ponderation.weight == 100 and not ponderation.final:
if not signature_date:
msg = ('Bravo, l\'affaire est signée! Merci de préciser à quelle date')
self.add_error('signature_date', msg)
if signature_date:
if self.instance.po_market:
po_market = self.instance.po_market
if po_market.signature_date:
if po_market.signature_date > signature_date:
formatted_date = po_market.signature_date.strftime('%d/%m/%y')
msg = f"Cette date doit être postérieure ou égale à la date de signature du marché de commande: {formatted_date}"
self.add_error('signature_date', msg)
if self.instance.framework_agreement:
framework_agreement = self.instance.framework_agreement
if framework_agreement.signature_date:
if framework_agreement.signature_date > signature_date:
formatted_date = framework_agreement.signature_date.strftime('%d/%m/%y')
msg = f"Cette date doit être postérieure ou égale à la date de signature de l\'accord cadre: {formatted_date}"
self.add_error('signature_date', msg)
if 'vat_rate' in self.fields:
if vat_rate is None:
# Rechercher le taux par défaut dans VATRates
default_vat_rate = VATRates.objects.filter(default_rate=True).first()
if default_vat_rate:
cleaned_data['vat_rate'] = default_vat_rate
# si pas de bénéficiaire saisi, c'est le client qui est bénéficiaire
if not beneficiary:
cleaned_data['beneficiary'] = customer
'''if self.errors:
# Copier les données du formulaire
data_copy = self.data.copy()
# Réaffecter les valeurs sélectionnées pour framework_agreement et po_market
framework_agreement = cleaned_data.get('framework_agreement')
po_market = cleaned_data.get('po_market')
data_copy['framework_agreement'] = framework_agreement
data_copy['po_market'] = po_market
# Assigner la copie modifiée à self.data
self.data = QueryDict('', mutable=True)
self.data.update(data_copy)'''
return cleaned_data
# fonctions d'initialisation des valeurs calculées
def init_calculated_fields(self):
if self.instance.pk:
self.fields['business_warning'].initial = self.instance.business_warning()
self.fields['amount_ratio'].initial = self.instance.amount_ratio()
self.fields['weighted_amount'].initial = self.instance.weighted_amount()
self.fields['amount_to_plan'].initial = self.instance.amount_to_plan()
self.fields['weighted_amount_ratio'].initial = self.instance.weighted_amount_ratio()
self.fields['weighted_amount_ratio_complement'].initial = self.instance.weighted_amount_ratio_complement()
self.fields['billed_amount'].initial = self.instance.billed_amount()
self.fields['billed_amount_ratio'].initial = self.instance.billed_amount_ratio()
self.fields['billed_amount_ratio_complement'].initial = self.instance.billed_amount_ratio_complement()
else:
self.fields['business_warning'].initial = False
self.fields['amount_ratio'].initial = 0
self.fields['weighted_amount'].initial = 0
self.fields['amount_to_plan'].initial = 0
self.fields['weighted_amount_ratio'].initial = 0
self.fields['weighted_amount_ratio_complement'].initial = 0
self.fields['billed_amount'].initial = 0
self.fields['billed_amount_ratio'].initial = 0
self.fields['billed_amount_ratio_complement'].initial = 0
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# La restiction des listes de choix de framework_agreement, des po_market,
# des contacts et adresse par défaut au client en cours est géré par Javascript
#if self.instance.pk
# On initialise les montants calculés
self.init_calculated_fields()
# Par défaut, on applique les mêmes conditions de règlement
p = Param.objects.get()
default_payment_period = p.payment_period
if p.payment_period and 'payment_period' in self.fields:
self.fields['payment_period'].initial= default_payment_period
# Format d'affichage des champs
optionalFields = ['vat_rate', 'payment_period', 'currency']
for field in optionalFields:
if field in self.fields:
self.fields[field].required = False
if 'vat_rate' in self.fields:
self.fields['vat_rate'].label = "Taux de TVA"
for _, value in self.fields.items():
value.widget.attrs['class'] = 'form-control text-primary'
if self.instance.pk:
if self.instance.amount_to_plan() != 0:
self.fields['amount_to_plan'].widget.attrs.update(
{'class': 'form-control text-danger ','style': 'font-weight: bold;'})
self.fields['name'].widget.attrs.update({'placeholder': 'Saisir une nouvelle affaire'})
self.fields['amount'].label = self.instance._meta.get_field('amount').verbose_name
self.fields['weighted_amount'].disabled = True
self.fields['amount_to_plan'].disabled = True
self.fields['amount_ratio']. disabled = True
self.fields['weighted_amount_ratio'].disabled = True
self.fields['weighted_amount_ratio_complement'].disabled = True
self.fields['billed_amount'].disabled = True
self.fields['billed_amount_ratio'].disabled = True
self.fields['billed_amount_ratio_complement'].disabled = True
if not self.instance.pk:
# si l'instance n'existe pas, on met la pondération au minimum
default_ponderation = BusinessStatus.objects.filter(final=False, facturable=False).order_by('weight').first()
if default_ponderation:
self.initial['ponderation'] = default_ponderation
else:
self.initial['ponderation'] = 2 #pk initial de "détectée"
else:
# On limite les options de framework_agreement à celles qui sont associées au client
customer = self.instance.customer
self.fields['framework_agreement'].queryset = Framework_agreement.objects.filter(customer=customer)
# On limite les options du champ po_market en fonction du framework agreement auquel est associé le business
if self.instance.framework_agreement:
self.fields['po_market'].queryset = PO_market.objects.filter(framework_agreement=self.instance.framework_agreement)
else:
self.fields['po_market'].queryset = PO_market.objects.filter(customer=customer, framework_agreement=None )
# On limite les options de default_contact, default_addressee et default_address à ceux qui sont associés au client
if 'default_contact' in self.fields:
self.fields['default_contact'].queryset = Contact.objects.filter(customer=customer)
if 'default_addressee' in self.fields:
self.fields['default_addressee'].queryset = Contact.objects.filter(customer=customer)
if 'default_address' in self.fields:
self.fields['default_address'].queryset = Address.objects.filter(customer=customer)
# si l'instance existe, que l'affaire est terminée ou perdue, les champs ne sont plus modifiables
# à l'exception du champ "ponderation"
if self.instance.ponderation.final == True:
print("INIT BUSINESS - affaire terminée perdue")
for fieldName, field in self.fields.items():
print("champ traité:", fieldName, field)
if fieldName != 'ponderation':
field.widget.attrs.update({'readonly': True, 'class': 'readonly-field form-control text-primary'})
else:
print("champ pondération")
field.widget.attrs.update({'readonly': False, 'class': 'form-control text-primary'})
weighted_amount = forms.DecimalField(disabled=True, label="Montant pondéré",widget=AmountInput)
amount_to_plan = forms.DecimalField(disabled = True, label="A planifier",widget=AmountInput)
amount_ratio = forms.DecimalField(required=False, label='ratio montant/100',widget=AmountInput)
weighted_amount_ratio =forms.DecimalField(required=False, label='ratio confiance')
weighted_amount_ratio_complement = forms.DecimalField(required=False, label = "complément")
billed_amount = forms.DecimalField(required = False,widget=AmountInput)
billed_amount_ratio = forms.CharField(required=False)
billed_amount_ratio_complement = forms.DecimalField(required=False,)
amount = forms.DecimalField(required=False,max_digits=10, decimal_places=2, localize=True)
business_warning = forms.BooleanField(required=False)
class Meta:
model = Business
fields = ['name', 'customer', 'beneficiary','framework_agreement','po_market', 'amount','ponderation','signature_date', 'account_exec',\
'customer_ref','engagement_number', 'organisational_unit', 'default_contact','default_addressee','default_address',\
'currency', 'vat_rate', 'payment_period','comment']
widgets = {
'creation_date': DatePickerInput,
'signature_date': DatePickerInput
}
The init limits the options of framework_agreement depending on the relations with the customer and the options of the po_markets depending on the framework_agreement.
On the client side, a javascrpt updates the po_market option every time the user changes the framework_agreement.
When the user changes the framework_agremeent and the po_market, the validation fails: po_market is not part of valid options. I have noticed thant while the request.POST has the expected value for po_market, it’s always “None” in the clean method.
I do not understand what’s going on! Where is the po_market value turned from the right value (the one in the request.POSt) to “None”???
I could let the init method with all possible options and rely on the the JS, but I don’t like it: it opens some security issues, doesn’it?
I’d rather have the server validate the po_market selected is in line with the framework_agreement.
Any help will be appreciated!
Richard