Django UpdateView Two Seperate Form Save (included inlineformset_factory)

I have a specific problem with my forms. I think it would be better to share my codes instead of explaining the problem in detail.

However, to explain in a nutshell; inside my model I have field OneToOneField and model of that field has inlineformset_factory form. My new model also has a form and I want to save both forms.

I get the following error when I want to save the offer update form:

TypeError at /ru/mytarget/offer-update/T2GTTT053E9/

AdminOfferUpdateView.form_invalid() missing 2 required positional arguments: ‘request_form’ and ‘request_item_formset’

Models:

request.py

class RequestModel(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="customer_requests")
    id = ShortUUIDField(primary_key=True, length=10, max_length=10, prefix="T", alphabet="ARGET0123456789", unique=True, editable=False)
    status = models.BooleanField(default=True)
    request_title = models.CharField(max_length=300)
    delivery_time = models.CharField(max_length=50)
    shipping_country = models.CharField(max_length=50)
    shipping_address = models.CharField(max_length=300)
    preferred_currency = models.ForeignKey(Currency, on_delete=models.CASCADE)
    shipping_term = models.ForeignKey(ShippingTerm, on_delete=models.CASCADE)
    delivery_term = models.ForeignKey(DeliveryTerms, on_delete=models.CASCADE)
    request_statuses = models.ForeignKey(RequestStatus, on_delete=models.CASCADE, blank=True, default=1)
    is_accepted = models.BooleanField(default=False)
    is_rejected = models.BooleanField(default=False)
    is_offer_created = models.BooleanField(default=False)
    updated_on = models.DateTimeField(auto_now=True)
    published_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return str(self.request_title)

    class Meta:
        verbose_name_plural = "Requests"
        verbose_name = "Request"

    def get_absolute_url(self):
        return reverse('mytarget:customer_request_details', kwargs={'pk': self.id})


class RequestItem(models.Model):
    request_model = models.ForeignKey(RequestModel, on_delete=models.CASCADE, related_name="request_items")
    product_name = models.CharField(max_length=300)
    product_info = models.TextField(max_length=2000, blank=True)
    target_price = models.DecimalField(max_digits=15, decimal_places=2, blank=True, null=True)
    price = models.DecimalField(max_digits=15, decimal_places=2, blank=True, null=True)
    quantity = models.DecimalField(max_digits=10, decimal_places=2)

    dimensions = models.CharField(max_length=100, blank=True)
    net_weight = models.CharField(max_length=20, blank=True)
    gross_weight = models.CharField(max_length=20, blank=True)
    hs_or_tn_ved_code = models.CharField(max_length=100, blank=True)
    brand = models.CharField(max_length=100, blank=True)
    manufacturer = models.CharField(max_length=100, blank=True)
    origin_country = models.CharField(max_length=50, blank=True)
    manufacturer_address = models.CharField(max_length=300, blank=True)

offer.py

class OfferModel(models.Model):
    request_model_name = models.OneToOneField(RequestModel, on_delete=models.CASCADE, primary_key=True)
    status = models.BooleanField(default=True)
    offer_validity = models.CharField(max_length=50, blank=True)
    terms_of_payment = models.CharField(max_length=100, blank=True, default=_("100% upon order"))
    profit_rate = models.PositiveIntegerField(blank=True, default=0)
    swift_payment_slip = models.FileField(upload_to='payment-slips/%Y/%m/%d/', validators=[max_file_size], blank=True)

    shipping_costs = models.CharField(max_length=300, blank=True)
    shipping_costs_quantity = models.PositiveIntegerField(blank=True, default=0)
    shipping_costs_price = models.DecimalField(max_digits=15, decimal_places=2, blank=True, default=0)
    additional_expenses = models.CharField(max_length=300, blank=True)
    additional_expenses_quantity = models.PositiveIntegerField(blank=True, default=0)
    additional_expenses_price = models.DecimalField(max_digits=15, decimal_places=2, blank=True, default=0)

    upload_shipping_details = models.FileField(upload_to='offer-files/%Y/%m/%d/', blank=True, null=True)
    upload_additional_documents = models.FileField(upload_to='offer-files/%Y/%m/%d/', blank=True, null=True)
    product_photo = models.ImageField(upload_to='offer-files/%Y/%m/%d/', blank=True, null=True)
    product_photo_thumbnail = ImageSpecField(source='product_photo',
                                             processors=[Transpose(),
                                                         SmartResize(width=2000, height=1500, upscale=False)
                                                         ],
                                             format='WEBP',
                                             options={'quality': 100})
    product_shipment_photo = models.ImageField(upload_to='offer-files/%Y/%m/%d/', blank=True, null=True)
    product_shipment_photo_thumbnail = ImageSpecField(source='product_shipment_photo',
                                                      processors=[Transpose(),
                                                                  SmartResize(width=2000, height=1500, upscale=False)
                                                                  ],
                                                      format='WEBP',
                                                      options={'quality': 100})
    shipping_tracking_code = models.CharField(max_length=500, blank=True)

    is_offer_accepted = models.BooleanField(default=False)
    is_offer_rejected = models.BooleanField(default=False)
    is_offer_canceled = models.BooleanField(default=False)
    send_proforma_invoice = models.BooleanField(default=False)
    is_payment_made = models.BooleanField(default=False)
    send_invoice = models.BooleanField(default=False)
    send_offer = models.BooleanField(default=False)
    is_detailed_offer = models.BooleanField(default=False)

    updated_on = models.DateTimeField(auto_now=True)
    published_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return str(self.request_model_name)

    class Meta:
        verbose_name_plural = "Offers"
        verbose_name = "Offer"

    def get_absolute_url(self):
        return reverse('mytarget:admin_offer_update', kwargs={'pk': self.request_model_name})

Forms:

request_create_form.py

class CustomerRequestForm(forms.ModelForm):

    disabled_fields = ("customer",)

    class Meta:
        model = RequestModel
        fields = ("customer", "request_title", "delivery_time", "shipping_country", "shipping_address",
                  "preferred_currency", "shipping_term", "delivery_term")

        widgets = {
            'request_title': TextInput(attrs={'class': 'form-control tableFormInputs',
                                              'placeholder': _('Example: Printers, Toner, and Cartridges')}),
            'delivery_time': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'shipping_country': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'shipping_address': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'preferred_currency': Select(attrs={'class': 'form-select tableFormInputs',
                                                'aria-label': _('Preferred Currency')}),
            'shipping_term': Select(attrs={'class': 'form-select tableFormInputs',
                                           'aria-label': _('Shipping Term')}),
            'delivery_term': Select(attrs={'class': 'form-select tableFormInputs',
                                           'aria-label': _('Delivery Terms')}),
        }

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('customer')
        super(CustomerRequestForm, self).__init__(*args, **kwargs)
        self.fields['preferred_currency'].queryset = self.fields['preferred_currency'].queryset.translated().order_by("translations__currency_name")
        self.fields['shipping_term'].queryset = self.fields['shipping_term'].queryset.translated().order_by("translations__shipping_term")

        for field in self.disabled_fields:
            self.fields[field].widget = forms.HiddenInput()
            self.fields[field].disabled = True


class CustomerRequestItemForm(forms.ModelForm):

    class Meta:
        model = RequestItem
        fields = ("product_name", "product_info", "target_price", "price", "quantity", "dimensions", "net_weight", "gross_weight",
                  "hs_or_tn_ved_code", "brand", "manufacturer", "origin_country", "manufacturer_address")
        exclude = ()

        widgets = {
            'product_name': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'product_info': Textarea(attrs={'class': 'form-control tableFormInputs', 'maxlength': 1000, 'rows': 3}),
            'target_price': NumberInput(attrs={'class': 'form-control tableFormInputs'}),
            'price': NumberInput(attrs={'class': 'form-control tableFormInputs'}),
            'quantity': NumberInput(attrs={'class': 'form-control tableFormInputs'}),

            'dimensions': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'net_weight': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'gross_weight': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'hs_or_tn_ved_code': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'brand': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'manufacturer': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'origin_country': TextInput(attrs={'class': 'form-control tableFormInputs'}),
            'manufacturer_address': TextInput(attrs={'class': 'form-control tableFormInputs'}),
        }


RequestItemInlineFormset = inlineformset_factory(RequestModel, RequestItem,
                                                 form=CustomerRequestItemForm,
                                                 extra=1,
                                                 can_delete=True
                                                 )

offer_update_form.py

class AdminOfferUpdateForm(forms.ModelForm):
    disabled_fields = ()
    hidden_fields = ("request_model_name",)

    request_title = forms.CharField(required=False, widget=TextInput(attrs={'class': 'form-control tableFormInputs', 'placeholder': _('Example: Printers, Toner, and Cartridges')}))
    delivery_time = forms.CharField(required=False, widget=TextInput(attrs={'class': 'form-control tableFormInputs'}))
    shipping_country = forms.CharField(required=False, widget=TextInput(attrs={'class': 'form-control tableFormInputs'}))
    shipping_address = forms.CharField(required=False, widget=Textarea(attrs={'class': 'form-control tableFormInputs', 'rows': 3}))
    preferred_currency = forms.ChoiceField(required=False, widget=Select(attrs={'class': 'form-select tableFormInputs', 'aria-label': _('Preferred Currency')}))
    shipping_term = forms.ChoiceField(required=False, widget=Select(attrs={'class': 'form-select tableFormInputs', 'aria-label': _('Shipping Term')}))
    delivery_term = forms.ChoiceField(required=False, widget=Select(attrs={'class': 'form-select tableFormInputs', 'aria-label': _('Delivery Terms')}))
    request_statuses = forms.ChoiceField(required=False, widget=Select(attrs={'class': 'form-select tableFormInputs', 'aria-label': _('Delivery Terms')}))

    class Meta:
        model = OfferModel
        fields = ("request_model_name", "offer_validity", "terms_of_payment", "profit_rate", "is_offer_accepted",
                  "is_offer_rejected", "send_proforma_invoice", "is_payment_made", "send_invoice", "send_offer",
                  "is_detailed_offer", "is_offer_canceled", "shipping_costs", "shipping_costs_quantity", "shipping_costs_price", "additional_expenses",
                  "additional_expenses_quantity", "additional_expenses_price", "upload_shipping_details",
                  "upload_additional_documents", "product_photo", "product_shipment_photo", "shipping_tracking_code",

                  "request_title", "delivery_time", "shipping_country", "shipping_address", "preferred_currency",
                  "shipping_term", "delivery_term", "request_statuses",
                  )

        widgets = {'offer_validity': TextInput(attrs={'class': 'form-control tableFormInputs'}),
                   'terms_of_payment': TextInput(attrs={'class': 'form-control tableFormInputs'}),
                   'profit_rate': NumberInput(attrs={'class': 'form-control tableFormInputs'}),

                   'shipping_costs': TextInput(attrs={'class': 'form-control tableFormInputs'}),
                   'shipping_costs_quantity': NumberInput(attrs={'class': 'form-control tableFormInputs'}),
                   'shipping_costs_price': NumberInput(attrs={'class': 'form-control tableFormInputs'}),
                   'additional_expenses': TextInput(attrs={'class': 'form-control tableFormInputs'}),
                   'additional_expenses_quantity': NumberInput(attrs={'class': 'form-control tableFormInputs'}),
                   'additional_expenses_price': NumberInput(attrs={'class': 'form-control tableFormInputs'}),

                   'upload_shipping_details': ClearableFileInput(attrs={'class': 'form-control w-auto tableFormInputs'}),
                   'upload_additional_documents': ClearableFileInput(attrs={'class': 'form-control w-auto tableFormInputs'}),
                   'product_photo': ClearableFileInput(attrs={'class': 'form-control w-auto tableFormInputs'}),
                   'product_shipment_photo': ClearableFileInput(attrs={'class': 'form-control w-auto tableFormInputs'}),
                   'shipping_tracking_code': TextInput(attrs={'class': 'form-control tableFormInputs'}),

                   'is_offer_accepted': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'is_offer_rejected': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'is_offer_canceled': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'send_proforma_invoice': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'is_payment_made': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'send_invoice': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'send_offer': CheckboxInput(attrs={'class': 'form-check-input'}),
                   'is_detailed_offer': CheckboxInput(attrs={'class': 'form-check-input'}),
                   }

    def __init__(self, *args, **kwargs):
        super(AdminOfferUpdateForm, self).__init__(*args, **kwargs)
        self.fields["preferred_currency"].choices = [(c.id, c.currency_name) for c in Currency.objects.all()]
        self.fields["shipping_term"].choices = [(st.id, st.shipping_term) for st in ShippingTerm.objects.all()]
        self.fields["delivery_term"].choices = [(dt.id, dt.delivery_term) for dt in DeliveryTerms.objects.all()]
        self.fields["request_statuses"].choices = [(r.id, r.status) for r in RequestStatus.objects.all()]

        for field in self.disabled_fields:
            self.fields[field].disabled = True

        for field in self.hidden_fields:
            self.fields[field].widget = forms.HiddenInput()

Views:

offer_update_view.py

@method_decorator([login_required(login_url=reverse_lazy("accounts:signin")), user_is_superuser], name='dispatch')
class AdminOfferUpdateView(UpdateView):
    model = OfferModel
    form_class = AdminOfferUpdateForm
    template_name = "mytarget/admin_offer_update.html"

    def get_context_data(self, **kwargs):
        context = super(AdminOfferUpdateView, self).get_context_data(**kwargs)
        if self.request.POST:
            context['request_form'] = AdminOfferUpdateForm(self.request.POST, instance=self.object.request_model_name)
            context['request_item_formset'] = RequestItemInlineFormset(self.request.POST, instance=self.object.request_model_name)
        else:
            context['request_form'] = AdminOfferUpdateForm(instance=self.object.request_model_name)
            context['request_item_formset'] = RequestItemInlineFormset(instance=self.object.request_model_name)
        return context

    def form_valid(self, form):
        context = self.get_context_data()
        request_form = context['request_form']
        request_item_formset = context['request_item_formset']
        with transaction.atomic():
            self.object = form.save()
            if request_form.is_valid() and request_item_formset.is_valid():
                request_form.instance = self.object.request_model_name
                request_form.save()
                request_item_formset.instance = self.object.request_model_name
                request_item_formset.save(commit=False)
                for ri in request_item_formset:
                    ri.save(commit=False)
                    request_item_formset.save()
        return super(AdminOfferUpdateView, self).form_valid(form)

    def form_invalid(self, form, request_form, request_item_formset):
        return self.render_to_response(
            self.get_context_data(form=form, request_form=request_form, request_item_formset=request_item_formset)
        )

    def get_initial(self):
        self.object = self.get_object()
        if self.object:
            return {"request_model": self.object.request_model_name, "request_item_formset": self.object.request_model_name}
        return super().initial.copy()

    def get_success_url(self):
        return reverse('mytarget:admin_offer_update', kwargs={'pk': self.object.id})

Side note: When asking for assistance with an error, please post the complete traceback. Frequently, the one-line summary is not sufficient information to diagnose a problem.

In this case the answer is fairly straight-forward - the function signature for form_invalid does not accept additional parameters. The post function will call form_invalid if the form is not valid, passing only form as a parameter.

<opinion> The Django-provided edit CBVs are not a good fit for a multiple-form or multiple-object structure. My experience has always been that it ends up being easier to either use a more basic base view or an FBV in those situations. </opinion>

Thank you very much Ken for the information and warning.

For now, I would like to ask a question in order not to create the form from scratch. I was able to fetch data from the other form into the form fields in the offer_update_view.py file. Ensuring the view is saved will fix the issue. I think there is a problem with my get_context_data and form_valid functions. So I’m getting the data from the inheritance form but I can’t save it in the new form. Do you have any ideas about this?

Recommendation: Rewrite this view from scratch as an FBV. It’s going to be a lot clearer as to how it works and you’re not going to be fighting a set of functions that aren’t designed for what you’re trying to do here. Your “you in six months” will appreciate it.
Just throw this away and redo it from scratch.

1 Like

OK, thank you for your feedbacks Ken. I will try from scratch.

Hello Ken,

The problem is mostly solved when starting from scratch. I was able to recreate the data in my new form by inheriting it from the other model. I just can’t render the data in the other model’s inlineformset_factory fields.

def create_offer_button(request):
    if request.method == "POST":
        post = request.POST.get("post_pk")
        obj = RequestModel.objects.get(pk=post)
        if obj.is_accepted:
            OfferModel.objects.create(request_model_name=obj,
                                      request_title=obj.request_title,
                                      delivery_time=obj.delivery_time,
                                      shipping_country=obj.shipping_country,
                                      shipping_address=obj.shipping_address,
                                      preferred_currency=obj.preferred_currency,
                                      shipping_term=obj.shipping_term,
                                      delivery_term=obj.delivery_term)

            obj.is_offer_created = True
            obj.save()
        return redirect(request.META.get("HTTP_REFERER"))

How can i access to RequestModel inlineformset_factory fields?

I solved it my own way.:

obj_items = RequestItem.objects.filter(request_model=obj)
for item in obj_items:
    OfferRequestItem.objects.create(offer_model_id=obj.id, product_name=item.product_name)