[Django]-Using F expression on django-money MoneyField

2👍

Since django-money 0.7.7 all basic F objects manipulations are supported.
It works for Django 1.4+.

0👍

Have you tried giving it a float instead of an integer? Try: wallet.balance.amount = F("balance__amount") + float(3)

I am not sure why their code doesn’t automatically try to convert integers to float’s if it that is what it requires.

0👍

For anyone using a serializer class, a possible source of problem (like for my case) wasn’t due to MoneyField but rather due to the failure in serializing F() expressions.

  File "/home/nponcian/Documents/GitHub/venv/lib/python3.8/site-packages/djmoney/contrib/django_rest_framework/fields.py", line 56, in to_representation
    return super().to_representation(obj)
  File "/home/nponcian/Documents/GitHub/venv/lib/python3.8/site-packages/rest_framework/fields.py", line 1148, in to_representation
    value = decimal.Decimal(str(value).strip())
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]

You can perform the F increment expression through this:

>>> obj = Wallet.objects.filter(balance_currency="EUR").first()
>>> obj.balance = F('balance') + Money(Decimal("12.24"), "EUR")
>>> obj.save()

So when using e.g. an update in a serializer class:

class WalletModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Wallet
        fields = "__all__"

    def update(self, instance: Wallet, validated_data: dict) -> Wallet:
        # Create a copy to not modify the original validation result
        validated_data = deepcopy(validated_data)

        if "balance" in validated_data:
            validated_data["balance"] += models.F("balance")
            # When the update is performed, the result is equivalent to performing:
            #     instance.balance = models.F("balance") + validated_data["balance"]
            # Or more specifically:
            #     instance.balance = models.F("balance") + djmoney.money.Money("12.34", "USD")

        return super().update(instance, validated_data)

But, calling serializer.save() and then accessing the serialized data via serializer.data which in turn calls serializer.to_representation() which in turn calls each of the field.to_representation() would fail for the field balance because its value is of type CombinedExpression e.g. <CombinedExpression: F(balance) + Value(12.34)> which can’t be serialized. Note that the expression is what we initially set as the value of the balance = F("balance") + Money("12.34", "USD"). As documented, F() assignments persist after Model.save()

F() objects assigned to model fields persist after saving the model instance and will be applied on each save()… This persistence can be avoided by reloading the model object after saving it, for example, by using refresh_from_db().

>>> # Access the balance after the obj.save()
>>> type(obj.balance)
<class 'django.db.models.expressions.CombinedExpression'>
>>> obj.balance
<CombinedExpression: F(balance) + Value(1.2)>

To solve this, depending on your requirements, you can just refresh it to fetch its latest value. As documented, the refresh would replace the CombinedExpression value into its actual value, which then could be successfully serialized.

>>> obj.refresh_from_db()
>>> type(obj.balance)
<class 'djmoney.money.Money'>
>>> obj.balance
Money('10.28', 'EUR')

Which is translated to this on a serializer class:

class WalletModelSerializer(serializers.ModelSerializer):
    ...
    def update(self, instance: Wallet, validated_data: dict) -> Wallet:
        ...
        instance = super().update(instance, validated_data)
        if any(isinstance(value, models.Expression) for value in validated_data.values()):
            instance.refresh_from_db()
        return instance

After this change, accessing serializer.data would be successful.

Leave a comment