Modify form field before saving it

Hello good people. I have a surely common problem but I can’t figure it out.

I have a basic Order <=> Products relation.

Models

class Product(model):
	### some fields   
    def get_price(self):
    	return x * 100 # Simplified
 
class Order(model):
	product = ManyToManyField(Product)
	total_price = models.DecimalField()
    
    def get_total_price(self):
    	return sum(p.get_product_price() for p in self.ordered_products.all())
        
class OrderProduct(model):
    product = ForeignKey(Product)
	order = ForeignKey(Order)
	quantity = PositiveIntegerField()
    
    def get_product_price(self):
    	return self.quantity * self.product.get_price()

What I’m trying:

  1. I want to save an Order Form with Products formsets (Product - Quantity)
  2. I need the price to be calculated before the form is saved.
    (I’m hiding the total_price field from the user)

Questions

I’m not sure where to put this logic:

  • I believe this answer from is one way, but I getting some errors on calling get_total_price inside form_valid()

  • Other question is: Do I need to include the total_price in my Order? My answer to this is that product prices will change in the future so I need to store somewhere the price of an Order that was made in the past.

Maybe there are better alternatives.

Thanks in advance! :wave:

  1. It’s probably going to be necessary to see the complete view where you tried this, along with the full error message and trace back you received.

  2. Whether you want to save the total price within the table depends upon whether or not you’re going to be able to accurately recalculate the total from the information available. (There are cases where the final charged price is not simply equal to the sum of the individual item prices - perhaps due to the application of sales taxes, or a quantity discount, or some type of coupon or credit being applied, etc, etc. Regardless of what factors are involved, you are always going to want to be able to track / identify the total amount changed and/or collected.)

Ken

Hi Ken.

  1. Those variables are the reasons why I keep it this way. I was thinking to maybe keep an historical registry of some models with this library for example.

On 1), I basically did in form_valid() what you pointed out:

class OrderCreateView(CreateView):
   model = Order
        
   def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        if self.request.POST:
            context["products"] = OrderProductsFormset(self.request.POST)
        else:
            context["products"] = OrderProductsFormset()
        return context

def form_valid(self, form):
        context = self.get_context_data()
        products_formset = context["products"]

        # Save the order
        new_order = form.save(commit=False)

        # I was wrongly calling the `get_total_price` method
        new_order.total_price = form.instance.get_total_price()

        new_order.save()

Now it works. Not sure if it’s the best way.

Hey, if you understand it, and it works (for any reasonable definition of “works”), then it’s the best way.

You’re not doing anything shady, you’re not monkey-patching core, you’re not relying upon unpublished APIs, you’re not writing unnecessary code - I think you’re good.

I suffer from Analysis Paralysis :sweat_smile:, so thanks for the confidence Ken.

For the sake of completeness, I believe form_valid should return the success URL.

Actually, for the sake of completeness and accuracy, form_valid should return an HttpResponse, which in the case of a form submission is usually an HttpResponseRedirect (but is not required to be).

But yes, unless you’re overriding post, the response is created by form_valid or form_invalid.

Thanks Ken for catching my error. That is what I meant to write.

Wow, this is old haha

The code below new_order.save() was this

products_formset.instance = new_order
products_formset.save()

return super().form_valid(form)

which executes this one

which is what Ken said.