Как использовать атрибут поля внешнего ключа для другого поля модели

У меня есть две модели в разных приложениях, например:

class Account(models.Model):
    """
    Class to store fiat account information of a companies bank account
    """
    number = models.CharField(max_length=100)
    currency = models.ForeignKey(FiatCurrency, on_delete=models.CASCADE)
    owner = models.ForeignKey(Company, on_delete=models.CASCADE)
    date_added = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.number


class FiatTransaction(models.Model):
    """
    Class to store Transactions made between escrow and operative white-listed fiat accounts
    """
    debit_account = models.ForeignKey('company.Account', on_delete=models.CASCADE, related_name='debit_account')
    credit_account = models.ForeignKey('company.Account', on_delete=models.CASCADE, related_name='credit_account')
    executed_on = models.DateTimeField(auto_now_add=True)
    amount = models.FloatField()
    currency = debit_account.currency
    is_processed = models.BooleanField(default=False)
    fee = models.FloatField()
    memo = models.CharField(max_length=250)

    def __str__(self):
        return F"Transferred {self.amount} from {self.debit_account} to {self.credit_account} at {self.executed_on}"

Теперь поле currency модели FiatTransaction, похоже, работает не так, как я задумал. Оно поднимает

AttributeError: объект 'ForeignKey' не имеет атрибута 'currency'

# Source model

class FiatCurrency(models.Model):
    """
    A model to store Fiat Currencies offered by Finchin to
    include into cash-pools.
    """
    ISO_Code = models.CharField(max_length=3)
    name = models.CharField(max_length=50)
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return self.name

Почему это так и как заставить это работать?

Вы можете сделать @property, который будет определять валюту этого объекта с помощью:

class FiatTransaction(models.Model):
    debit_account = models.ForeignKey('company.Account', on_delete=models.CASCADE, related_name='debit_account')
    credit_account = models.ForeignKey('company.Account', on_delete=models.CASCADE, related_name='credit_account')
    executed_on = models.DateTimeField(auto_now_add=True)
    amount = models.FloatField()
    is_processed = models.BooleanField(default=False)
    fee = models.FloatField()
    memo = models.CharField(max_length=250)
    
    @property
    def currency(self):
        return self.debit_account.currency

Однако это может быть неэффективно, если вам приходится делать это для большого количества FiatTransactions.

В этом случае лучше удалить свойство currency и аннотировать QuerySet с помощью:

from django.db.models import F

FiatTransaction.objects.annotate(currency=F('debit_account__currency'))

Возникающие при этом FiatTransaction будут иметь дополнительный атрибут .currency, который будет содержать .currency из .debit_account.

Если вам это нужно часто, вы можете воспользоваться Manager, который будет автоматически аннотировать при обращении к FiatTransaction.objects:

from django.db.models import F

class FiatTransactionManager(models.Manager):
    
    def get_queryset(self, *args, **kwargs):
        return super().get_queryset(*args, **kwargs).annotate(
            currency=F('debit_account__currency')
        )

class FiatTransaction(models.Model):
    # …
    objects = FiatTransactionManager()
Вернуться на верх