pythondjangosignalsmanytomanyfield

Django calculating field depending on selection with manytomany field


I am writing a web application using django rest.

I have a problem that i am struggling with for a few days now.

So i created a payroll model that calculates the gross value based on the number of hours worked and hourly rate by the employee and its working perfectly.

The main problem is that i want to calculate net value based on taxes and fixed costs (which are seperated models) choosen by user. Actually i made function that use post_save signal and it works, but i have to save object twice to calculate it correctly because if i save for the first time it sees old selections (i think its happening because function is called too fast and there is no time to create a relation between models).Earlier i used the function with m2mchanged signal, but there was a lot of code and if statements besides it didnt work well.

Models:

PERCENTAGE_VALIDATOR = [MinValueValidator(0), MaxValueValidator(100)]


class Taxes(models.Model):
    tax_name = models.CharField(max_length=100)
    tax_percentage = models.DecimalField(max_digits=5, decimal_places=2, validators=PERCENTAGE_VALIDATOR)

    class Meta:
        verbose_name = 'Tax'
        verbose_name_plural = 'Taxes'

    def __str__(self):
        return f'{self.tax_name} - {self.tax_percentage}%'

class FixedCosts(models.Model):
    name = models.CharField(max_length=100)
    value = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)

    class Meta:
        verbose_name = 'Fixed Cost'
        verbose_name_plural = 'Fixed Costs'

    def __str__(self):
        return f'{self.name} - {self.value}eur'
class Payroll(models.Model):
    payroll_for_user = models.ForeignKey(MyUser, on_delete=models.CASCADE)
    payroll_month = models.DateField()
    payroll_hourly_rate = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
    payroll_net_value = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) #net_value = gross_value - taxes
    payroll_gross_value = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
    payroll_amount_of_hours_worked = models.IntegerField()
    payroll_taxes = models.ManyToManyField(Taxes, blank=True, related_name="payroll_taxes")
    payroll_fixed_costs = models.ManyToManyField(FixedCosts, blank=True, related_name="payroll_fixed_costs")
    payroll_send = models.BooleanField(default=False)
    payroll_send_date = models.DateTimeField(blank=True, null=True)

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

    def get_gross_value_based_on_hours(self):
        gross_value = int(self.payroll_hourly_rate * self.payroll_amount_of_hours_worked)
        return gross_value

    def get_hourly_rate(self):
        hourly_rate = self.payroll_for_user.hourly_rate
        return hourly_rate

    def save(self, *args, **kwargs):
        if not self.payroll_hourly_rate:
            self.payroll_hourly_rate = self.get_hourly_rate()

        if not self.payroll_gross_value:
            self.payroll_gross_value = self.get_gross_value_based_on_hours()

        if not self.payroll_net_value:
            self.payroll_net_value = self.get_gross_value_based_on_hours()

        super(Payroll, self).save(*args, **kwargs)

Signal function

@receiver(post_save, sender=Payroll)
def get_net_value(sender, instance, **kwargs):
    gross_value = instance.payroll_gross_value
    payroll = Payroll.objects.filter(id=instance.id)
    net_value = False
    if instance.payroll_taxes.all().count() > 0:
        selected_taxes = instance.payroll_taxes.all().aggregate(Sum('tax_percentage'))
        total_taxes = selected_taxes['tax_percentage__sum'] / 100
        gross_value = gross_value - (gross_value * total_taxes)

    if instance.payroll_fixed_costs.all().count() > 0:
        fixed_costs = instance.payroll_fixed_costs.all().aggregate(Sum('value'))
        total_fixed_cost = fixed_costs['value__sum']
        net_value = gross_value - total_fixed_cost

    if net_value:
        payroll.update(payroll_net_value=net_value)
    else:
        payroll.update(payroll_net_value=gross_value)


Solution

  • Just in case you want a second opinion. You can clean up your models, there are many redundant fields in Payroll such as payroll_gross_value is the same as the method get_gross_value_based_on_hours. An option is to use a model property.

    Also, you are also unnecessarily overriding the save method with redundant conditions by replacing the field with itself. Lastly, you can also rename your models fields to improve code readability.

    models.py

    PERCENTAGE_VALIDATOR = [MinValueValidator(0), MaxValueValidator(100)]
    
    class Taxes(models.Model):
        name = models.CharField(max_length=100)
        percentage = models.DecimalField(
            max_digits=5, 
            decimal_places=2, 
            validators=PERCENTAGE_VALIDATOR
        )
    
        class Meta:
            verbose_name = 'Tax'
            verbose_name_plural = 'Taxes'
    
        def __str__(self):
            return f'{self.name} - {self.percentage}%'
    
    class FixedCosts(models.Model):
        name = models.CharField(max_length=100)
        value = models.DecimalField(
            max_digits=10, 
            decimal_places=2, 
            blank=True, 
            null=True
        )
    
        class Meta:
            verbose_name = 'Fixed Cost'
            verbose_name_plural = 'Fixed Costs'
    
        def __str__(self):
            return f'{self.name} - {self.value} €'
        
    class Payroll(models.Model):
        user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
        date = models.DateField()
        hourly_rate = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
        worked_hours = models.IntegerField()
        taxes = models.ManyToManyField(Taxes, blank=True, related_name="payroll_taxes")
        fixed_costs = models.ManyToManyField(FixedCosts, blank=True, related_name="payroll_fixed_costs")
        send = models.BooleanField(default=False)
        send_date = models.DateTimeField(blank=True, null=True)
    
        def __str__(self):
            return f'Payroll for {self.user.username}'
    
        @property
        def gross_value(self):
            return self.hourly_rate * self.worked_hours
        
        @property
        def net_value(self):
            taxes = self.taxes.all()
            fixed_costs = self.fixed_costs.all()
            net_value = self.gross_value
            
            for tax in taxes:
                net_value -= (self.gross_value * tax.percentage) / 100
    
            for cost in fixed_costs:
                net_value -= cost.value
            
            return net_value
    

    tests.py (A simple test case)

    class PayrollTestCase(TestCase):
        def setUp(self):
            self.user = get_user_model().objects.create_user(username='test', password='test123')
            self.payroll = Payroll.objects.create(
                user=self.user, 
                date=datetime.datetime.now(), 
                worked_hours=160, 
                hourly_rate=decimal.Decimal(10)
            )
    
        def test_payroll_net_value_property(self):
            fixed_cost1 = FixedCosts.objects.create(
                name='Fixed Cost One', 
                value=decimal.Decimal(100)
            )
            fixed_cost2 = FixedCosts.objects.create(
                name='Fixed Cost Two', 
                value=decimal.Decimal(50)
            )
    
            tax1 = Taxes.objects.create(name='Tax One', percentage=10)
            tax2 = Taxes.objects.create(name='Tax Two', percentage=20)
    
            self.payroll.fixed_costs.add(fixed_cost1)
            self.payroll.fixed_costs.add(fixed_cost2)
            self.payroll.taxes.add(tax1)
            self.payroll.taxes.add(tax2)
    
            self.assertEqual(self.payroll.net_value, 970)
        
        def test_payroll_gross_value(self):
            self.assertEqual(self.payroll.gross_value, 1600)