text: import decimal
  
  from crispy_forms.helper import FormHelper
  from crispy_forms.layout import Layout, Div, Field, HTML
  from django import forms
  from django.core.exceptions import ValidationError
  from django.db.models import Sum
  from django.forms import BaseInlineFormSet, inlineformset_factory
  from django.utils.safestring import mark_safe
  
  from grant_management.forms import utils
  from grant_management.forms.invoices import html_message
  from grant_management.models import Installment, Invoice
  from project_core.fields import FlexibleDecimalField
  from project_core.models import Project
  from project_core.templatetags.thousands_separator import thousands_separator
  from project_core.widgets import XDSoftYearMonthDayPickerInput
  
  
  class InstallmentModelForm(forms.ModelForm):
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
  
          message = ''
          self.fields['can_be_deleted'] = forms.CharField(initial=1, required=False)
          if self.instance and self.instance.id and Invoice.objects.filter(installment=self.instance).exists():
              message = 'This installment cannot be deleted because invoices are attached to it'
              self.fields['can_be_deleted'].initial = 0
  
          self.helper = FormHelper()
          self.helper.form_tag = False
          self.helper.disable_csrf = True  # checked in the higher form level
  
          self.helper.layout = Layout(
              Div(
                  Div('project', hidden=True),
                  Div('id', hidden=True),
                  Div(Field('DELETE', hidden=True)),
                  Div('can_be_deleted', hidden=True, css_class='can_be_deleted'),
                  css_class='row', hidden=True
              ),
              Div(
                  Div('amount', css_class='col-4'),
                  Div(HTML(html_message(message)), css_class='col-4 to-delete'),
                  css_class='row'
              )
          )
  
      def clean(self):
          cd = super().clean()
  
          if self.errors:
              return cd
  
          project = cd['project']
  
          project_starts = project.start_date
          project_ends = project.end_date
  
          amount = cd.get('amount', None)
  
          DELETE = cd.get('DELETE', None)
  
          errors = {}
  
          total_invoiced = None
  
          if self.instance:
              total_invoiced = self.instance.invoice_set.all().aggregate(Sum('amount'))['amount__sum']
  
          if amount and total_invoiced and amount < total_invoiced:
              errors[
                  'amount'] = f'New installment amount {thousands_separator(amount)} CHF is lower than currently invoiced to this installment ({thousands_separator(total_invoiced)} CHF)'
  
          if amount > project.allocated_budget:
              errors['amount'] = 'This amount is greater than the total project allocated budget'
  
          if DELETE and Invoice.objects.filter(installment=self.instance).exists():
              errors['DELETE'] = 'Cannot delete Installment: there are invoices assigned to this installment'
  
          if project.is_active() is False:
              raise ValidationError(f'Cannot modify installments for this project: the status is {project.status}')
  
          if errors:
              raise ValidationError(errors)
  
          return cd
  
      class Meta:
          model = Installment
          fields = ['project', 'amount']
          field_classes = {'amount': FlexibleDecimalField}
          labels = {'amount': 'Amount (CHF)'}
  
  
  class InstallmentsFormSet(BaseInlineFormSet):
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
  
          self.helper = FormHelper()
          self.helper.form_tag = False
  
      def clean(self):
          super().clean()
  
          if not self.is_valid():
              # if one of the budget items is not valid: doesn't validate the general form
              # E.g. if an amount is negative it will have an error in the amount but the
              # amount is removed from the form.cleaned_data
              return
  
          budget_amount = decimal.Decimal('0.00')
          maximum_budget = self.instance.allocated_budget
  
          for installment_form in self.forms:
              amount = installment_form.cleaned_data['amount'] or 0
  
              budget_amount += amount
  
          if budget_amount > maximum_budget:
              raise forms.ValidationError(
                  f'Total allocated budget for this project is {thousands_separator(maximum_budget)} CHF.'
                  f' The current total of the installments is {thousands_separator(budget_amount)} CHF which exceeds the allocated budget.'
              )
  
      def get_queryset(self):
          return super().get_queryset().order_by('id')
  
      def extra_information(self):
          return mark_safe(f'<p><b>Budget allocated</b>: {thousands_separator(self.instance.allocated_budget)} CHF</p>')
  
  
  InstallmentsInlineFormSet = inlineformset_factory(Project, Installment, form=InstallmentModelForm,
                                                    formset=InstallmentsFormSet,
                                                    min_num=1, extra=0, can_delete=True)
  