[Answered ]-Django calculating field depending on selection with manytomany field

1πŸ‘

βœ…

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)
πŸ‘€Niko

0πŸ‘

I figured it out on my own,

@receiver(m2m_changed, sender=Payroll.payroll_taxes.through)
@receiver(m2m_changed, sender=Payroll.payroll_fixed_costs.through)
def calculate_net_value(sender, instance, **kwargs):
    gross_value = instance.payroll_gross_value
    taxes_total = instance.payroll_taxes.all().aggregate(Sum('tax_percentage'))['tax_percentage__sum'] or 0
    taxes_total = Decimal(taxes_total / 100)
    fixed_costs_total = instance.payroll_fixed_costs.all().aggregate(Sum('value'))['value__sum'] or 0
    instance.payroll_net_value = gross_value - (gross_value * taxes_total)\
                         - fixed_costs_total
    instance.save()
πŸ‘€Bartosz

Leave a comment