Saving a model on another model save()

Hello everyone. Hope you’re doing well!

Models

  • I have 2 models, Invoice and Visit (Medical Consultation) with a OneToOne Relation
class Invoice(models.Model):
     visit = models.OneToOneField(Visit)

What I need

On saving Visit, I need to create an instance of Invoice with the data of the visit.

Not the best way but I can accomplish it with:

# If Visit does not have a Invoice, create it
if not hasattr(Visit, 'Invoice'):
                    
    tmp_invoice = Invoice(
        # Save the fields with the values from Visit
        date = visit.date
    )
    tmp_invoice.save()

else:
    # If there exist a relation, update Invoice
    tmp_invoice(
        id=visit.invoice.pk # Here I use the OneToOne relation to access the pk
        date = visit.date
    )
    new_invoice.save()

The problem

I think there may be a better solution to this because I’m basically duplicating code. Also I feel the OneToOne relation may be helpful, but I just can’t see a better solution.

Thanks.

1 Like

Django has a super neat model object method called update_or_create().

https://docs.djangoproject.com/en/3.1/ref/models/querysets/

In your case, say you wanted to create or update an invoice, you could execute the following:

Invoice.objects.update_or_create(
    id=<id>,
    defaults={"date":visit.date}
)

This will look for a record with an id=<id> and then update the items in the default dict if the record exists, else it will create one.

The caveat with this method is you have to have something unique, and which you create yourself which that you can use as the lookup field. So ideally you would manage Invoice.id yourself.

If you can’t use the ID as the lookup as they’re created automatically, you can define your own unique constraints in your model, which you can then use when looking up an object.

For example:

class Invoicel(models.Model):
   id = auto int
   date = date
   dogs_name = 
   cats_name =

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["dogs_name", "cats_name"], name="unique_invoice"
            )
        ]

You could then do a lookup like this:

Invoice.objects.update_or_create(
    cats_name=name,
    dogs_name=name,
    defaults={
        "date":visit.date,
        "dogs_name": new_name,
        "cats_name": another_new_name
    }
)

Saying all that, if the above code is only written once in your app, then I see little wrong with it. It effectively does what the update_or_create() method does, although there is more error handling in the update_or_create() method.

Another thing which you can look at is overriding the save() method of Visit and do some logic there. As an example:

class Visit(models.Mode):
     attribute = 
     etc etc etc

    def save(self, *args, **kwargs):
        # do something with Invoice here
        return super().save(*args, **kwargs)

And if you really must, you can look at using Django’s signals.

Hope some of this may be of help.

3 Likes

Wow, thanks for your detailed answer Conor.

I was aware of get_or_create method but didn’t know about the update one.

  • In my case I can use the visit as the unique constraint because of the OneToOne relation.
  • Also, every field of the Visit can be changed so I’m not using the defaults arg.

I replaced the code above with the following:

invoice, created = Invoice.objects.update_or_create(
    visit=visit,
    pacient=visit.pacient,
    professional=visit.professional
    ### Other fields

I manually tested and it worked correctly on creating and updating a Visit and it’s corresponding Invoice. Now I need to write real tests. :sweat_smile:


This saving is part of a form with 4 formsets so my question is: is there a good way to pass the data to the save() method of Visit?

Again, thanks for the explanation Conor.

1 Like

Glad it helped, Marco,

Just to be doubly sure, the update_or_create() method only updates the fields with the values in the default dictionary. It will not use the kwargs for updating. As an exampe:

# use visit, patient and professional to fetch an invoice
# This will only every fetch or create something.
# It won't, according to my experience and the docs, update anything.
invoice, created = Invoice.objects.update_or_create(
    visit=visit,
    pacient=visit.pacient,
    professional=visit.professional
)

On the other hand, this will update patient and professional.

# use visit to fetch an invoice
# If Invoice with visit exists, update all fields in the defaults {}
# If invoice with visit doesn't exist, create an Invoice with the fields, values in defaults {}
invoice, created = Invoice.objects.update_or_create(
    visit=visit,
    defaults={
       "patient": visit.patient,
       "professional": visit.professional,
        etc etc
    }
)

Note also that you can query for an object based on a kwarg, in your case visit, and also update it by including it in the defaults dict.

Take a look at the update_or_create() docs in the link I sent. The relevant paragraph is:

The update_or_create method tries to fetch an object from database based on the given kwargs . If a match is found, it updates the fields passed in the defaults dictionary.

Actually, now that I’m just reading your post again, I think I’ve misunderstood you. You’re saying that you don’t need to update the Invoice because you can update all its information by changing the value of the fields in a Visit instance? Have I understood you correctly? And apologies if I have.

As for the save() method from a FormSet and Form, I’m going to have to plead ignorance, as I spend all my time in Django Rest Framework and have very limited experience with forms so everything I write here should be taken with a grain of salt. And perhaps there is someone wiser who can give you some advice here.

Let’s say you are creating a Visit as you have defined above, you can override it’s save method to create or update an Invoice. I should say that this is just an idea, I’m sure there are others who might know of a more elegant way of doing it directly from interaction with the Formset

class Visit(models.Model):
	id = primary_key
	patient = 
	professional =

	def save(self, *args, **kwargs):
		self.do_something_to_invoice()
		return super().save(*args, **kwargs)

	def do_something_to_invoice(self):
		invoice, _ = Invoice.objects.update_or_create(
			   	visit=self.id,
			   	defaults={
			   		"patient":self.patient,
    				"professional"=self.professional
			   }

		) 

Did I answer your question or. am I way off?

Cheers,

Conor

2 Likes

Hello Conor.
I have to appreciate the time you took to explain to me again!

I just made another test updating some Visit fields to find out I was incorrectly using the method as you said.

duplicate key value violates unique constraint 
"appName_invoice_visit_id_key"
DETAIL:  Key (visit_id)=(1) already exists.

I modified my code with your example and now it updates the Invoice correctly. I’ll make sure to write tests this time. :sweat_smile:


About the formset advice

I remember thinking about moving all this saving process to the model like the code you post.
What I didn’t know at that time was how to pass all the necessary data, mainly because it was deeply coupled with the formsets.
Now I see it’s not so hard as I thought.

Again, thanks for your explanation!

You’re welcome, Marco, and glad it all worked out. Best of luck with the project!

Cheers,

Conor

1 Like