lingo/lingo/invoicing/forms.py

864 lines
29 KiB
Python

# lingo - payment and billing system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import django_filters
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from gadjo.forms.widgets import MultiSelectWidget
from lingo.agendas.models import Agenda
from lingo.invoicing.models import (
Campaign,
DraftInvoice,
DraftInvoiceLine,
DraftJournalLine,
Invoice,
InvoiceLine,
InvoicePayment,
JournalLine,
Payer,
Payment,
PaymentType,
Regie,
)
from lingo.utils.wcs import get_wcs_options
class ExportForm(forms.Form):
regies = forms.BooleanField(label=_('Regies'), required=False, initial=True)
payers = forms.BooleanField(label=_('Payers'), required=False, initial=True)
class ImportForm(forms.Form):
config_json = forms.FileField(label=_('Export File'))
class RegieForm(forms.ModelForm):
class Meta:
model = Regie
fields = [
'label',
'slug',
'description',
'payer',
'cashier_role',
]
def clean_slug(self):
slug = self.cleaned_data['slug']
if Regie.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(_('Another regie exists with the same identifier.'))
return slug
class PaymentTypeForm(forms.ModelForm):
class Meta:
model = PaymentType
fields = ['label', 'slug', 'disabled']
def clean_slug(self):
slug = self.cleaned_data['slug']
if self.instance.regie.paymenttype_set.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(_('Another payment type exists with the same identifier.'))
return slug
class CampaignForm(forms.ModelForm):
class Meta:
model = Campaign
fields = ['label', 'date_start', 'date_end', 'injected_lines', 'agendas']
widgets = {
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'agendas': MultiSelectWidget,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['agendas'].queryset = self.instance.regie.agenda_set.all()
def clean(self):
cleaned_data = super().clean()
if 'date_start' in cleaned_data and 'date_end' in cleaned_data:
if cleaned_data['date_end'] <= cleaned_data['date_start']:
self.add_error('date_end', _('End date must be greater than start date.'))
elif 'agendas' in cleaned_data:
new_date_start = cleaned_data['date_start']
new_date_end = cleaned_data['date_end']
new_agendas = cleaned_data['agendas']
overlapping_qs = (
Campaign.objects.filter(regie=self.instance.regie)
.exclude(pk=self.instance.pk)
.extra(
where=['(date_start, date_end) OVERLAPS (%s, %s)'],
params=[new_date_start, new_date_end],
)
)
for agenda in new_agendas:
if overlapping_qs.filter(agendas=agenda).exists():
self.add_error(
None,
_('Agenda "%s" has already a campaign overlapping this period.') % agenda.label,
)
return cleaned_data
def save(self):
super().save(commit=False)
if self.instance._state.adding:
# init date fields
self.instance.date_publication = self.instance.date_end
self.instance.date_payment_deadline = self.instance.date_end
self.instance.date_due = self.instance.date_end
self.instance.date_debit = self.instance.date_end
elif self.instance.pool_set.exists():
self.instance.mark_as_invalid(commit=False)
self.instance.save()
self._save_m2m()
return self.instance
class CampaignDatesForm(forms.ModelForm):
class Meta:
model = Campaign
fields = ['date_publication', 'date_payment_deadline', 'date_due', 'date_debit']
widgets = {
'date_publication': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_payment_deadline': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_due': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_debit': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
def clean(self):
cleaned_data = super().clean()
if (
'date_publication' in cleaned_data
and 'date_payment_deadline' in cleaned_data
and 'date_due' in cleaned_data
):
if cleaned_data['date_publication'] > cleaned_data['date_payment_deadline']:
self.add_error(
'date_payment_deadline', _('Payment deadline must be greater than publication date.')
)
elif cleaned_data['date_payment_deadline'] > cleaned_data['date_due']:
self.add_error('date_due', _('Due date must be greater than payment deadline.'))
return cleaned_data
def save(self):
super().save()
draft_invoice_qs = DraftInvoice.objects.filter(pool__campaign=self.instance)
invoice_qs = Invoice.objects.filter(pool__campaign=self.instance)
for qs in [draft_invoice_qs, invoice_qs]:
qs.update(
date_publication=self.instance.date_publication,
date_payment_deadline=self.instance.date_payment_deadline,
date_due=self.instance.date_due,
)
qs.filter(payer_direct_debit=True).update(date_debit=self.instance.date_debit)
return self.instance
class AgendaFieldsFilterSetMixin:
def _init_agenda_fields(self, line_model, invoice_queryset):
lines = line_model.objects.filter(invoice__in=invoice_queryset).values('slug', 'details__agenda')
agenda_slugs = set()
for line in lines:
if not line['details__agenda'] and '@' not in line['slug']:
continue
agenda_slugs.add(line['details__agenda'] or line['slug'].split('@')[0])
# get agendas from slugs
agendas = Agenda.objects.filter(slug__in=agenda_slugs)
# and init agenda filter choices
self.filters['agenda'].field.choices = [(a.slug, a.label) for a in agendas]
# get line details to build event filter choices
agenda_labels_by_slug = {a.slug: a.label for a in agendas}
lines = line_model.objects.filter(invoice__in=invoice_queryset).values(
'slug', 'label', 'details__primary_event', 'details__agenda'
)
events = set()
for line in lines:
# build a slug with agenda slug and event slug
slug = (
'%s@%s' % (line['details__agenda'], line['details__primary_event'])
if line['details__agenda']
else line['slug']
)
if '@' not in slug:
# no information about agenda, injected line ?
continue
agenda_slug = slug.split('@')[0]
if agenda_slug not in agenda_labels_by_slug:
# unknown agenda slug
continue
events.add(
(
slug,
'%s / %s' % (agenda_labels_by_slug[agenda_slug], line['label']),
agenda_labels_by_slug[agenda_slug],
line['label'],
)
)
# build event filter choices
events = sorted(list(events), key=lambda e: (e[2], e[3]))
self.filters['event'].field.choices = [(e[0], e[1]) for e in events]
def filter_agenda(self, queryset, name, value):
if not value:
return queryset
line_model = InvoiceLine
if hasattr(self, 'pool') and self.pool.draft:
line_model = DraftInvoiceLine
lines = line_model.objects.filter(
Q(details__agenda=value) | Q(slug__startswith='%s@' % value)
).values('invoice')
return queryset.filter(pk__in=lines)
def filter_event(self, queryset, name, value):
if not value:
return queryset
line_model = InvoiceLine
if hasattr(self, 'pool') and self.pool.draft:
line_model = DraftInvoiceLine
agenda_slug, event_slug = value.split('@')
lines = line_model.objects.filter(
Q(details__agenda=agenda_slug, details__primary_event=event_slug) | Q(slug=value)
).values('invoice')
return queryset.filter(pk__in=lines)
class AbstractInvoiceFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
# for Invoice
number = django_filters.CharFilter(
label=_('Invoice number'),
field_name='formatted_number',
lookup_expr='contains',
)
# for DraftInvoice
pk = django_filters.NumberFilter(
label=_('Invoice number'),
)
payment_number = django_filters.CharFilter(
label=_('Payment number'),
method='filter_payment_number',
)
payer_external_id = django_filters.CharFilter(
label=_('Payer (external ID)'),
)
payer_first_name = django_filters.CharFilter(
label=_('Payer first name'),
lookup_expr='icontains',
)
payer_last_name = django_filters.CharFilter(
label=_('Payer last name'),
lookup_expr='icontains',
)
payer_demat = django_filters.BooleanFilter(
label=_('Payer demat'),
)
payer_direct_debit = django_filters.BooleanFilter(
label=_('Payer direct debit'),
)
user_external_id = django_filters.CharFilter(
label=_('User (external ID)'),
method='filter_user_external_id',
)
user_first_name = django_filters.CharFilter(
label=_('User first name'),
method='filter_user_first_name',
)
user_last_name = django_filters.CharFilter(
label=_('User last name'),
method='filter_user_last_name',
)
total_amount_min = django_filters.LookupChoiceFilter(
label=_('Total amount min'),
field_name='total_amount',
field_class=forms.DecimalField,
empty_label=None,
lookup_choices=[
('gt', '>'),
('gte', '>='),
],
)
total_amount_max = django_filters.LookupChoiceFilter(
label=_('Total amount max'),
field_name='total_amount',
field_class=forms.DecimalField,
empty_label=None,
lookup_choices=[
('lt', '<'),
('lte', '<='),
],
)
paid = django_filters.ChoiceFilter(
label=_('Paid'),
widget=forms.RadioSelect,
empty_label=_('all'),
choices=[
('yes', _('Totally')),
('partially', _('Partially')),
('no', _('No')),
],
method='filter_paid',
)
agenda = django_filters.ChoiceFilter(
label=_('Activity'),
empty_label=_('all'),
method='filter_agenda',
)
event = django_filters.ChoiceFilter(
label=_('Event'),
empty_label=_('all'),
method='filter_event',
)
def __init__(self, *args, **kwargs):
self.pool = kwargs.pop('pool')
super().__init__(*args, **kwargs)
if self.pool.draft:
del self.filters['number']
del self.filters['payment_number']
del self.filters['paid']
else:
del self.filters['pk']
line_model = InvoiceLine
if self.pool.draft:
line_model = DraftInvoiceLine
self._init_agenda_fields(line_model, self.queryset)
def filter_payment_number(self, queryset, name, value):
return queryset.filter(
pk__in=InvoicePayment.objects.filter(payment__formatted_number__contains=value).values('invoice')
)
def filter_user_external_id(self, queryset, name, value):
if not value:
return queryset
line_model = InvoiceLine
if self.pool.draft:
line_model = DraftInvoiceLine
lines = line_model.objects.filter(user_external_id=value).values('invoice')
return queryset.filter(pk__in=lines)
def filter_user_first_name(self, queryset, name, value):
if not value:
return queryset
line_model = InvoiceLine
if self.pool.draft:
line_model = DraftInvoiceLine
lines = line_model.objects.filter(user_first_name__icontains=value).values('invoice')
return queryset.filter(pk__in=lines)
def filter_user_last_name(self, queryset, name, value):
if not value:
return queryset
line_model = InvoiceLine
if self.pool.draft:
line_model = DraftInvoiceLine
lines = line_model.objects.filter(user_last_name__icontains=value).values('invoice')
return queryset.filter(pk__in=lines)
def filter_paid(self, queryset, name, value):
if value == 'yes':
return queryset.filter(remaining_amount=0)
if value == 'partially':
return queryset.filter(remaining_amount__gt=0, paid_amount__gt=0)
if value == 'no':
return queryset.filter(paid_amount=0)
return queryset
class DraftInvoiceFilterSet(AbstractInvoiceFilterSet):
class Meta:
model = DraftInvoice
fields = []
class InvoiceFilterSet(AbstractInvoiceFilterSet):
class Meta:
model = Invoice
fields = []
class AbstractJournalLineFilterSet(django_filters.FilterSet):
# for JournalLine
invoice_number = django_filters.CharFilter(
label=_('Invoice number'),
field_name='invoice_line__invoice__formatted_number',
lookup_expr='contains',
)
# for DraftJournalLine
invoice_id = django_filters.NumberFilter(
label=_('Invoice number'),
field_name='invoice_line__invoice_id',
)
invoice_line = django_filters.NumberFilter(
label=_('Invoice line'),
)
payer_external_id = django_filters.CharFilter(
label=_('Payer (external ID)'),
)
payer_first_name = django_filters.CharFilter(
label=_('Payer first name'),
lookup_expr='icontains',
)
payer_last_name = django_filters.CharFilter(
label=_('Payer last name'),
lookup_expr='icontains',
)
payer_demat = django_filters.BooleanFilter(
label=_('Payer demat'),
)
payer_direct_debit = django_filters.BooleanFilter(
label=_('Payer direct debit'),
)
user_external_id = django_filters.CharFilter(
label=_('User (external ID)'),
)
user_first_name = django_filters.CharFilter(
label=_('User first name'),
lookup_expr='icontains',
)
user_last_name = django_filters.CharFilter(
label=_('User last name'),
lookup_expr='icontains',
)
agenda = django_filters.ChoiceFilter(
label=_('Activity'),
empty_label=_('all'),
method='filter_agenda',
)
event = django_filters.ChoiceFilter(
label=_('Event'),
empty_label=_('all'),
method='filter_event',
)
status = django_filters.ChoiceFilter(
label=_('Status'),
widget=forms.RadioSelect,
empty_label=_('all'),
choices=[
('success', _('Success')),
('success_injected', _('Success (Injected)')),
('warning', _('Warning')),
('error', _('Error')),
('error_todo', _('Error (To treat)')),
('error_ignored', _('Error (Ignored)')),
('error_fixed', _('Error (Fixed)')),
],
method='filter_status',
)
def __init__(self, *args, **kwargs):
self.pool = kwargs.pop('pool')
super().__init__(*args, **kwargs)
if self.pool.draft:
del self.filters['invoice_number']
else:
del self.filters['invoice_id']
agenda_slugs = list(set(self.queryset.values_list('event__agenda', flat=True)))
agendas = Agenda.objects.filter(slug__in=agenda_slugs)
self.filters['agenda'].field.choices = [(a.slug, a.label) for a in agendas]
agenda_labels_by_slug = {a.slug: a.label for a in agendas}
lines = self.queryset.values('event__agenda', 'event__slug', 'event__primary_event', 'label')
events = set()
for line in lines:
if line['event__agenda'] not in agenda_labels_by_slug:
continue
events.add(
(
'%s@%s' % (line['event__agenda'], line['event__primary_event'] or line['event__slug']),
'%s / %s' % (agenda_labels_by_slug.get(line['event__agenda']), line['label']),
agenda_labels_by_slug.get(line['event__agenda']),
line['label'],
)
)
events = sorted(list(events), key=lambda e: (e[2], e[3]))
self.filters['event'].field.choices = [(e[0], e[1]) for e in events]
def filter_agenda(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(event__agenda=value)
def filter_event(self, queryset, name, value):
if not value:
return queryset
agenda_slug, event_slug = value.split('@')
return queryset.filter(
Q(event__primary_event=event_slug) | Q(event__slug=event_slug), event__agenda=agenda_slug
)
def filter_status(self, queryset, name, value):
if not value:
return queryset
if value == 'success_injected':
return queryset.filter(status='success', from_injected_line__isnull=False)
if value == 'error_todo':
return queryset.filter(status='error', error_status='')
if value == 'error_ignored':
return queryset.filter(status='error', error_status='ignored')
if value == 'error_fixed':
return queryset.filter(status='error', error_status='fixed')
return queryset.filter(status=value)
class DraftJournalLineFilterSet(AbstractJournalLineFilterSet):
class Meta:
model = DraftJournalLine
fields = []
class JournalLineFilterSet(AbstractJournalLineFilterSet):
class Meta:
model = JournalLine
fields = []
class DateRangeWidget(django_filters.widgets.DateRangeWidget):
def __init__(self, attrs=None):
widgets = (
forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
)
super(django_filters.widgets.SuffixedMultiWidget, self).__init__(widgets, attrs)
class DateRangeField(django_filters.fields.DateRangeField):
widget = DateRangeWidget
class DateFromToRangeFilter(django_filters.DateFromToRangeFilter):
field_class = DateRangeField
class RegieInvoiceFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
number = django_filters.CharFilter(
label=_('Invoice number'),
field_name='formatted_number',
lookup_expr='contains',
)
created_at = DateFromToRangeFilter(
label=_('Creation date'),
field_name='created_at',
)
date_payment_deadline = DateFromToRangeFilter(
label=_('Payment deadline'),
field_name='date_payment_deadline',
)
date_due = DateFromToRangeFilter(
label=_('Due date'),
field_name='date_due',
)
payment_number = django_filters.CharFilter(
label=_('Payment number'),
method='filter_payment_number',
)
payer_external_id = django_filters.CharFilter(
label=_('Payer (external ID)'),
)
payer_first_name = django_filters.CharFilter(
label=_('Payer first name'),
lookup_expr='icontains',
)
payer_last_name = django_filters.CharFilter(
label=_('Payer last name'),
lookup_expr='icontains',
)
payer_demat = django_filters.BooleanFilter(
label=_('Payer demat'),
)
payer_direct_debit = django_filters.BooleanFilter(
label=_('Payer direct debit'),
)
user_external_id = django_filters.CharFilter(
label=_('User (external ID)'),
method='filter_user_external_id',
)
user_first_name = django_filters.CharFilter(
label=_('User first name'),
method='filter_user_first_name',
)
user_last_name = django_filters.CharFilter(
label=_('User last name'),
method='filter_user_last_name',
)
total_amount_min = django_filters.LookupChoiceFilter(
label=_('Total amount min'),
field_name='total_amount',
field_class=forms.DecimalField,
empty_label=None,
lookup_choices=[
('gt', '>'),
('gte', '>='),
],
)
total_amount_max = django_filters.LookupChoiceFilter(
label=_('Total amount max'),
field_name='total_amount',
field_class=forms.DecimalField,
empty_label=None,
lookup_choices=[
('lt', '<'),
('lte', '<='),
],
)
paid = django_filters.ChoiceFilter(
label=_('Paid'),
widget=forms.RadioSelect,
empty_label=_('all'),
choices=[
('yes', _('Totally')),
('partially', _('Partially')),
('no', _('No')),
],
method='filter_paid',
)
agenda = django_filters.ChoiceFilter(
label=_('Activity'),
empty_label=_('all'),
method='filter_agenda',
)
event = django_filters.ChoiceFilter(
label=_('Event'),
empty_label=_('all'),
method='filter_event',
)
class Meta:
model = Invoice
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._init_agenda_fields(InvoiceLine, self.queryset)
def filter_payment_number(self, queryset, name, value):
return queryset.filter(
pk__in=InvoicePayment.objects.filter(payment__formatted_number__contains=value).values('invoice')
)
def filter_user_external_id(self, queryset, name, value):
if not value:
return queryset
lines = InvoiceLine.objects.filter(user_external_id=value).values('invoice')
return queryset.filter(pk__in=lines)
def filter_user_first_name(self, queryset, name, value):
if not value:
return queryset
lines = InvoiceLine.objects.filter(user_first_name__icontains=value).values('invoice')
return queryset.filter(pk__in=lines)
def filter_user_last_name(self, queryset, name, value):
if not value:
return queryset
lines = InvoiceLine.objects.filter(user_last_name__icontains=value).values('invoice')
return queryset.filter(pk__in=lines)
def filter_paid(self, queryset, name, value):
if value == 'yes':
return queryset.filter(remaining_amount=0)
if value == 'partially':
return queryset.filter(remaining_amount__gt=0, paid_amount__gt=0)
if value == 'no':
return queryset.filter(paid_amount=0)
return queryset
class RegiePaymentFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
number = django_filters.CharFilter(
label=_('Payment number'),
field_name='formatted_number',
lookup_expr='contains',
)
created_at = DateFromToRangeFilter(
label=_('Date'),
field_name='created_at',
)
invoice_number = django_filters.CharFilter(
label=_('Invoice number'),
method='filter_invoice_number',
)
payer_external_id = django_filters.CharFilter(
label=_('Payer (external ID)'),
)
payer_first_name = django_filters.CharFilter(
label=_('Payer first name'),
lookup_expr='icontains',
)
payer_last_name = django_filters.CharFilter(
label=_('Payer last name'),
lookup_expr='icontains',
)
payment_type = django_filters.ChoiceFilter(
label=_('Payment type'),
widget=forms.RadioSelect,
empty_label=_('all'),
)
amount_min = django_filters.LookupChoiceFilter(
label=_('Amount min'),
field_name='amount',
field_class=forms.DecimalField,
empty_label=None,
lookup_choices=[
('gt', '>'),
('gte', '>='),
],
)
amount_max = django_filters.LookupChoiceFilter(
label=_('Amount max'),
field_name='amount',
field_class=forms.DecimalField,
empty_label=None,
lookup_choices=[
('lt', '<'),
('lte', '<='),
],
)
agenda = django_filters.ChoiceFilter(
label=_('Activity'),
empty_label=_('all'),
method='filter_agenda',
)
event = django_filters.ChoiceFilter(
label=_('Event'),
empty_label=_('all'),
method='filter_event',
)
class Meta:
model = Payment
fields = []
def __init__(self, *args, **kwargs):
self.regie = kwargs.pop('regie')
super().__init__(*args, **kwargs)
self.filters['payment_type'].field.choices = [(t.pk, t) for t in self.regie.paymenttype_set.all()]
invoice_queryset = Invoice.objects.filter(
pk__in=InvoicePayment.objects.filter(payment__in=self.queryset).values('invoice')
)
self._init_agenda_fields(InvoiceLine, invoice_queryset)
def filter_invoice_number(self, queryset, name, value):
return queryset.filter(
pk__in=InvoicePayment.objects.filter(invoice__formatted_number__contains=value).values('payment')
)
def filter_agenda(self, queryset, name, value):
if not value:
return queryset
lines = InvoiceLine.objects.filter(
Q(details__agenda=value) | Q(slug__startswith='%s@' % value)
).values('invoice')
return queryset.filter(pk__in=InvoicePayment.objects.filter(invoice__in=lines).values('payment'))
def filter_event(self, queryset, name, value):
if not value:
return queryset
agenda_slug, event_slug = value.split('@')
lines = InvoiceLine.objects.filter(
Q(details__agenda=agenda_slug, details__primary_event=event_slug) | Q(slug=value)
).values('invoice')
return queryset.filter(pk__in=InvoicePayment.objects.filter(invoice__in=lines).values('payment'))
class NewPayerForm(forms.ModelForm):
carddef_reference = forms.ChoiceField(
label=_('Linked card model'),
required=False,
)
class Meta:
model = Payer
fields = [
'label',
'description',
'carddef_reference',
'payer_external_id_prefix',
'payer_external_id_template',
'payer_external_id_from_nameid_template',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
card_models = get_wcs_options('/api/cards/@list')
self.fields['carddef_reference'].choices = [('', '-----')] + card_models
class PayerForm(NewPayerForm):
class Meta:
model = Payer
fields = [
'label',
'slug',
'description',
'carddef_reference',
'payer_external_id_prefix',
'payer_external_id_template',
'payer_external_id_from_nameid_template',
]
def clean_slug(self):
slug = self.cleaned_data['slug']
if Payer.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(_('Another payer exists with the same identifier.'))
return slug
class PayerMappingForm(forms.ModelForm):
class Meta:
model = Payer
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.cached_carddef_json:
return
for key, label in self.instance.user_variables:
self.fields[key] = forms.ChoiceField(
label=label,
choices=[('', '-----')] + [(k, v) for k, v in self.instance.carddef_fields.items()],
required=False,
)
def save(self):
self.instance.user_fields_mapping = {k: self.cleaned_data[k] for k, v in self.instance.user_variables}
self.instance.save()
return self.instance