from datetime import date from django.db import models from django.db.models import Q, Sum from decimal import Decimal class Animal(models.Model): is_active = models.BooleanField(default=True) identifier = models.CharField(max_length=45) birth = models.DateField() weight = models.DecimalField(max_digits=10, decimal_places=2) farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='animals') def __str__(self): return f"{self.identifier}" @property def age(self): today = date.today() birth_date = self.birth years = today.year - birth_date.year months = today.month - birth_date.month if today.day < birth_date.day: months -= 1 if months < 0: months += 12 years -= 1 if years > 0 and months > 0: return f"{years} anos e {months} meses" # YYYY years and MM months elif years > 0: return f"{years} anos" # YYYY years elif months > 0: return f"{months} meses" # MM months else: return "Menos de um mês" # Less than a month @property def is_in_batch(self): return self.batchanimal_set.filter(is_active=True).exists() class Meta: constraints = [ models.UniqueConstraint( fields=['identifier', 'farm'], condition=Q(is_active=True), name='unique_active_farm_animal' ) ] class BatchType(models.Model): is_active = models.BooleanField(default=True) farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='batch_types') name = models.CharField(max_length=45) consumption_factor = models.DecimalField(max_digits=4, decimal_places=2, default=1.00) def __str__(self): return f"{self.name} ({self.consumption_percentage}%)" @property def consumption_percentage(self): value = int((self.consumption_factor - 1) * 100) if value >= 0: return f'+{value}' return f'{value}' class Batch(models.Model): is_active = models.BooleanField(default=True) identifier = models.CharField(max_length=45) animal_category = models.CharField(max_length=45, null=True, blank=True) batch_type = models.ForeignKey(BatchType, on_delete=models.SET_NULL, related_name='batches', null=True, blank=True) animals = models.ManyToManyField(Animal, through='BatchAnimal', related_name='batches') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__original_is_active = self.is_active def save(self, *args, **kwargs): super().save(*args, **kwargs) is_active_changed = self.__original_is_active != self.is_active if is_active_changed: if self.is_active is False: self.batchanimal_set.all().update(is_active=False) elif self.is_active is True: self.batchanimal_set.all().update(is_active=True) self.__original_is_active = self.is_active @property def total_ugm(self): if self.animals: total_weight = self.animals.aggregate( total=Sum("weight") )["total"] or 0 return Decimal(f"{total_weight / 500:.2f}") def __str__(self): return self.identifier class BatchAnimal(models.Model): animal = models.ForeignKey(Animal, on_delete=models.CASCADE) batch = models.ForeignKey(Batch, on_delete=models.CASCADE) is_active = models.BooleanField(default=True) # When reactivating a batch, all its animals must not belong to any other active batch. # Otherwise, the database will raise an IntegrityError due to the unique constraint on active animals. # This is a rare use case, but any future code regarding it must implement error treatment class Meta: constraints = [ models.UniqueConstraint( fields=['animal'], condition=Q(is_active=True), name='unique_active_animal_batch_link' ) ] def __str__(self): return f"{self.animal.identifier} in {self.batch.identifier}" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__original_is_active = self.is_active def save(self, *args, **kwargs): is_active_changed = ( self.__original_is_active != self.is_active ) if is_active_changed and self.is_active: raise ValueError( "Reactivating a BatchAnimal is not allowed automatically because it may violate " "the unique active batch constraint for an animal. Review the logic in " "models/animal.py (BatchAnimal.save) before proceeding." ) super().save(*args, **kwargs) self.__original_is_active = self.is_active class FeedingRecord(models.Model): animal = models.ForeignKey(Animal, on_delete=models.CASCADE, related_name='feeding_record') rotation = models.ForeignKey('entities.Rotation', on_delete=models.CASCADE, related_name='feeding_record') feed_quantity = models.DecimalField(max_digits=10, decimal_places=2) def __str__(self): return f"{self.animal.identifier} - {self.feed_quantity}kg"