Home > Enterprise >  How to use foreign key field's attribute for another model field
How to use foreign key field's attribute for another model field

Time:11-16

I have two models in different apps like so:

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}"

Now the field currency of model FiatTransaction doesn't seem to work the way I intend it to do. It raises

AttributeError: 'ForeignKey' object has no attribute '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

Why's that and how to make this work?

CodePudding user response:

You can make a @property that will determine the currency of that object with:

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

This can however be inefficient if you have to do this for a lot of FiatTransactions.

In that case it might be better to remove the currency property, and annotate the QuerySet with:

from django.db.models import F

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

The FiatTransactions that arise from this will have an extra attribute named .currency that will contain the .currency of the .debit_account.

If you need this often, you can make use of a Manager that will automatically annotate when you access 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()
  • Related