Calculate and save the total of an invoice in django - django

When I add an invoice, the Total is always 0 but when I update without any changes, it's updated with the totalsubtotals(). I understand that there are many calculations and in my case, the total calculation is done before the subtotals. Any recommendations.
class Invoice(models.Model):
date = models.DateField(default=timezone.now)
client = models.ForeignKey('Client',on_delete=models.PROTECT)
total = models.DecimalField(default=0, max_digits=20, decimal_places=2)
def totalsubtotals(self):
items = self.invoiceitem_set.all()
total = 0
for item in items:
total += item.subtotal
return total
def save(self, *args, **kwargs):
self.total = self.totalsubtotals()
super(Invoice, self).save(*args, **kwargs)
class InvoiceItem(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
price = models.DecimalField(max_digits=20, decimal_places=2)
quantity = models.DecimalField(max_digits=20, decimal_places=2)
subtotal = models.DecimalField(default=0, max_digits=20, decimal_places=2)
def save(self, *args, **kwargs):
self.subtotal = self.price * self.quantity
super(InvoiceItem, self).save(*args, **kwargs)

It looks to me like your "default = 0" in your InvoiceItem model under subtotal is what's causing the problem, if there is any error with price or quantity the default value is stored, returning 0 to your Invoice model.
I find that default values also make debugging a lot harder so I try to only use them where values are optional, in the case of an invoice, you can't order no quantity of products and you can't have no price either (0 is a number) errors in the input would set the value in the DB to Null (or None in the case of Python) and then your default sets the subtotal to 0.
Removing the default would cause errors when you try to input your values and you can better track down where the issue is based on your error messages.
Alternatively, in your save function for InvoiceItem, you can try...
if self.price && self.quantity: (check that they're not Null/None)
self.subtotal = self.price * self.quantity
else:
raise ValueError('Incorrect values in price or subtotal')

Related

Django - Adding up values in an object's many to many field?

I have 2 classes
class Service(models.Model):
service_name = models.CharField(max_length=15, blank=False)
service_time = models.IntegerField(blank=False)
class Appointment(models.Model):
name = models.CharField(max_length=15)
service_selected = models.ManytoManyField(Service, blank=True)
total_time = models.IntegerField(null=True, blank=True)
What Im trying to do is that after a user has selected the services and created an appointment, each of the services' service_time will be added up to equal total_time
I know I have to use something along the lines of
#in class Appointment
def save(self, *args, **kwargs):
self.total_time += self.service_selected.service_time #it needs to add up all of the service_time from each service selected but that's where Im lost
super(Appointment, self).save(*args, **kwargs)
But I don't know how to get the service_time value from each of the service_name that were selected, or how to query for every service_selected chosen in that appointment's object so that I can add up those values.
edit:
Had to change
total_time = models.IntegerField(blank=False)
to
total_time = models.IntegerField(blank=False, null=False, default=0)
for the answer to work
You can do it as follows:
#in class Appointment
def save(self, *args, **kwargs):
self.total_time += Service.objects.all().aggregate(total_time=Sum('service_time'))['total_time']
# You will have to run a query to get the data and aggregate according to that. You may change the query according to your needs.
super(Appointment, self).save(*args, **kwargs)

Calculate with agregates and save on table field on django

How to calculate the total of invoice using sum annotate or agregate instead of forloop and save the value on field invoice.total
class Invoice(models.Model):
date = models.DateField(default=timezone.now)
client = models.ForeignKey('Client',on_delete=models.PROTECT)
total = models.DecimalField(default=0, max_digits=20, decimal_places=2)
def totalsubtotals(self):
items = self.invoiceitem_set.all()
total = 0
for item in items:
total += item.subtotal
return total
def save(self, *args, **kwargs):
self.total = self.totalsubtotals()
super(Invoice, self).save(*args, **kwargs)
class InvoiceItem(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
price = models.DecimalField(max_digits=20, decimal_places=2)
quantity = models.DecimalField(max_digits=20, decimal_places=2)
subtotal = models.DecimalField(default=0, max_digits=20, decimal_places=2)
def save(self, *args, **kwargs):
self.subtotal = self.price * self.quantity
super(InvoiceItem, self).save(*args, **kwargs)
Please don't. This is a form of data duplication: you store some part of the data twice: both the subtotals and the total. In fact subtotal is not necessary either.
It turns out that keeping data in sync is a hard problem. Imagine that you later change the quantity of an InvoiceItem, then you will not save the Invoice again, and thus the total is no longer correct. If you remove an InvoiceItem or you create a new InvoiceItem, then that is not the case either. You can make use of signals, but a lot of Django ORM calls will not run the signals. Indeed for example:
from django.db.models import F
InvoiceItem.objects.filter(pk=14).update(quantity=F('quantity')+1)
Here we thus increment a quantity, but the subtotal and total will not be updated, not even if we use signals.
You can try to implement this at the database level through triggers, but that will require manually adding triggers, and it is furthermore still likely you forget a scenario then eventually the data will get out of sync.
It is therefore more convenient not to use duplicated data. You can for example calculate the subtotal through a property:
class InvoiceItem(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
price = models.DecimalField(max_digits=20, decimal_places=2)
quantity = models.DecimalField(max_digits=20, decimal_places=2)
#property
def subtotal(self):
return self.price * self.quantity
for the Invoice you should not calculate this at the Django/Pyton layer, since that will take too much time and result in extra querying. You can use .annotate(…) in the views where you need the total of the Invoices with:
from django.db.models import F, Sum
Invoice.objects.annotate(
total=Sum(F('invoiceitem__quantity') * F('price'))
)
Your Invoice model thus does not contain a total or any subroutines to recalculate the total:
class Invoice(models.Model):
date = models.DateField(default=timezone.now)
client = models.ForeignKey('Client',on_delete=models.PROTECT)

Action when a foreign key is being changed on django

The question in general is about finding the modification of a foreign key of a model and call some function of the related model.
Assume I have two model class:
class Discount(models.Model):
def use(self, sell_item):
if self.max_price:
self.max_price -= sell_item.net()
if self.max_count:
self.max_count -= sell_item.amount
self.save()
def deuse(self, sell_item):
if self.max_price:
self.max_price += sell_item.net()
if self.max_count:
self.max_count += sell_item.amount
self.save()
max_price = models.PositiveIntegerField(blank=True,
null=True)
max_count = models.PositiveIntegerField(blank=True,
null=True)
amount = models.PositiveIntegerField(blank=False,
null=False)
class SellItem(models.Model):
def net(self):
price = self.amount * self.price
if self.discount:
price -= self.discount.amount * price / 100
return price * (1 + self.tax / 100)
amount = models.PositiveIntegerField(balnk=False,
null=False)
price = models.PositiveIntegerField(blank=False,
null=False)
tax = models.PositiveIntegerFeidl(blank=False,
null=False)
discount = models.ForeignKey(Discount,
blank=True,
null=True)
Now I want to execute use function whenever a discount add to an item and deuse it whenever it is being removed from an item. I found a post about it and to do that I write below code for sell item:
def __init__(self, *args, **kwargs):
self.dirty = False
self.pre_states = []
self.new_states = []
super(SellItem, self).__init__(*args, **kwargs)
def __setattr__(self, name, value):
if name == 'discount':
if hasattr(self, name):
pre_discount = self.discount
if pre_discount != value:
self.dirty = True
if pre_discount:
self.pre_states = ['pre_discount']
self.pre_discount = pre_discount
if value:
self.new_states = ['discount']
object.__setattr__(self, name, value)
def save(self, *args, **kwargs):
super(SellItem, self).save(*args, **kwargs)
if self.dirty:
if 'pre_discount' in self.pre_states:
self.pre_discount.deuse(self)
if 'discount' in self.new_states:
self.discount.use(self)
But it is not enough, because basically django would not fetch a foreign key when a new class is constructed, it instead just fill the _id item for it and whenever you need that it would fetch it from database, if I check for modification of discount_id instead of discount based on the order of setting of member values I may miss the previous discount because I have just current and previous discount_id not discount.
I know that it could possible implement with checking all of cases but I think after all I depend on django implementation of the behavior of database fetching which could be changed further.
I think there must be a proper and easier solution for just knowing the modification of a foreign key, I know there is some packages for storing history of modification but they are too much for my simple request.

Record Total Value in Django

Record the value of a calculated field is really more efficient to calculate without saving?
class SaleDetail(models.Model):
product = models.ForeignKey(Product)
quantity = models.IntegerField()
price = models.DecimalField(max_digits=8, decimal_places=2)
subtotal = models.DecimalField(max_digits=8, decimal_places=2)
def save(self, *args, **kwargs):
self.subtotal = self.price * self.quantity
super(SaleDetail, self).save(*args, **kwargs)
Calculated field
class SaleDetail(models.Model):
product = models.ForeignKey(Product)
quantity = models.IntegerField()
price = models.DecimalField(max_digits=8, decimal_places=2)
def get_total_value(self):
if self.quantity:
return self.price * self.quantity
total_value = property(get_total_value)
There doesn't really seem to be any good reason to pre-calculate the field here. Multiplying one existing value by another is a simple operation, and there's no benefit to be gained by calculating it on save.
There would only be some benefit if the value to be calculated involved a lot of complex and expensive operations, such as querying multiple tables or calling an external API.

How to bind custom function to model field?

Say this is my simple models.py
class Order(models.Model):
quantity = models.IntegerField()
item_price = models.FloatField()
I'd like to have a function to calculate the total price:
def calc_total(self):
return self.quantity * self.item_price
Now, how do I create model field total_price so it gets populated automatically in database? I know I can use calc_total method in template but I need this in db also.
Override the save method in your model to set the value each time the model is saved:
class Order(models.Model):
quantity = models.IntegerField()
item_price = models.FloatField()
total_price = models.FloatField()
def calc_total(self):
return self.quantity * self.item_price
def save(self, *args, **kwargs):
self.total_price = self.calc_total()
super(Order, self).save(*args, **kwargs)